@drafthq/draft 3.1.5 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ # _graph_queries.sh — canonical Cypher builders + fail-loud runner for the
3
+ # codebase-memory-mcp engine. Sourced by the graph-*.sh wrappers; never executed.
4
+ #
5
+ # Single source of query truth (graph-tooling-v2 Guardrail 2): no Cypher literal
6
+ # should live in a wrapper entrypoint. A label or dialect fix is a one-line edit
7
+ # here, not a hunt across N scripts (the Phase 0 :Function bug was duplicated
8
+ # across two files precisely because the Cypher was inlined).
9
+ #
10
+ # Dialect notes (engine v0.8.x, verified live against this engine):
11
+ # SAFE : fixed-length patterns, single/multi-hop explicit patterns, `=`, `<`,
12
+ # `STARTS WITH`, `NOT x STARTS WITH`, `AND`, `OR`, relationship-type
13
+ # alternation `[:A|B]`, simple `count(x)`.
14
+ # UNSAFE : coalesce(), `<>` / `!=` / `<=` / `>=`, `NOT EXISTS(...)`,
15
+ # `NOT (pattern)`, `WITH`-grouping aggregation, multi-pattern joins.
16
+ # Every builder below stays inside the SAFE set.
17
+ #
18
+ # Label-agnostic on name matches: code units are :Method ⪢ :Function in OO repos;
19
+ # pinning :Function silently returns [] (the graph-tooling-v2 Phase 0 bug). CALLS
20
+ # edges only connect callables, so dropping the label stays precise.
21
+
22
+ # shellcheck shell=bash
23
+
24
+ # Pull in memory_cli / find_memory_bin / MEMORY_BIN if a wrapper sourced only us.
25
+ _GQ_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ if [[ "$(type -t memory_cli 2>/dev/null)" != function ]]; then
27
+ # shellcheck source=_lib.sh
28
+ source "$_GQ_DIR/_lib.sh"
29
+ fi
30
+
31
+ # Escape single quotes for embedding inside a Cypher single-quoted string literal.
32
+ gq_escape() {
33
+ local s="$1"
34
+ printf '%s' "${s//\'/\\\'}"
35
+ }
36
+
37
+ # ── Cypher builders. Each echoes one query string. $1 = pre-escaped symbol. ──
38
+
39
+ gq_q_exists() { printf "MATCH (f {name:'%s'}) RETURN f.name AS name LIMIT 1" "$1"; }
40
+ gq_q_callers() { printf "MATCH (c)-[:CALLS]->(f {name:'%s'}) RETURN c.name AS caller, c.file_path AS file LIMIT 200" "$1"; }
41
+ gq_q_callers_prod() { printf "MATCH (c)-[:CALLS]->(f {name:'%s'}) WHERE c.is_test=false AND NOT c.file_path STARTS WITH 'tests/' RETURN c.name AS caller, c.file_path AS file LIMIT 200" "$1"; }
42
+ gq_q_callers_qualified() { printf "MATCH (c)-[:CALLS]->(f {qualified_name:'%s'}) RETURN c.name AS caller, c.file_path AS file LIMIT 200" "$1"; }
43
+ gq_q_cycles2() { printf "MATCH (a)-[:CALLS]->(b)-[:CALLS]->(a) WHERE a.qualified_name < b.qualified_name RETURN a.qualified_name AS a, b.qualified_name AS b LIMIT 100"; }
44
+ gq_q_cycles3() { printf "MATCH (a)-[:CALLS]->(b)-[:CALLS]->(c)-[:CALLS]->(a) RETURN a.qualified_name AS a, b.qualified_name AS b, c.qualified_name AS c LIMIT 100"; }
45
+ gq_q_tests() { printf "MATCH (t)-[:TESTS]->(f {name:'%s'}) RETURN t.qualified_name AS test, t.file_path AS file LIMIT 200" "$1"; }
46
+ gq_q_tested_all() { printf "MATCH (t)-[:TESTS]->(f) RETURN f.qualified_name AS symbol LIMIT 2000"; }
47
+ gq_q_exported() { printf "MATCH (f) WHERE f.is_exported=true RETURN f.qualified_name AS symbol, f.file_path AS file LIMIT 2000"; }
48
+ gq_q_imports() { printf "MATCH (a)-[:IMPORTS]->(b) RETURN a.file_path AS src, b.file_path AS dst LIMIT 1000"; }
49
+ gq_q_co_change() { printf "MATCH (a:File)-[r:FILE_CHANGES_WITH]->(b:File) RETURN a.name AS src, b.name AS dst, r.coupling_score AS score ORDER BY r.coupling_score DESC LIMIT 40"; }
50
+ gq_q_inherits() { printf "MATCH (c)-[:INHERITS]->(p) RETURN c.qualified_name AS child, p.qualified_name AS parent LIMIT 500"; }
51
+ gq_q_inherits_sym() { printf "MATCH (c)-[:INHERITS]->(p) WHERE c.name='%s' RETURN c.qualified_name AS child, p.qualified_name AS parent LIMIT 200" "$1"; }
52
+ gq_q_derived_sym() { printf "MATCH (c)-[:INHERITS]->(p) WHERE p.name='%s' RETURN c.qualified_name AS child, p.qualified_name AS parent LIMIT 200" "$1"; }
53
+ gq_q_writes() { printf "MATCH (f {name:'%s'})-[:WRITES]->(v) RETURN v.name AS target, v.file_path AS file LIMIT 200" "$1"; }
54
+ gq_q_raises() { printf "MATCH (f {name:'%s'})-[:RAISES|THROWS]->(e) RETURN e.name AS error, e.qualified_name AS qualified LIMIT 200" "$1"; }
55
+ gq_q_raisers() { printf "MATCH (f)-[:RAISES|THROWS]->(e {name:'%s'}) RETURN f.qualified_name AS raiser, f.file_path AS file LIMIT 200" "$1"; }
56
+ gq_q_node_props() { printf "MATCH (f) RETURN f.qualified_name AS q, f.complexity AS c, f.cognitive AS cog, f.is_entry_point AS ep LIMIT 10000"; }
57
+ gq_q_risk() { printf "MATCH (f) WHERE f.unguarded_recursion=true OR f.alloc_in_loop=true OR f.recursion_in_loop=true OR f.linear_scan_in_loop=true RETURN f.qualified_name AS symbol, f.file_path AS file, f.complexity AS complexity, f.unguarded_recursion AS unguarded_recursion, f.alloc_in_loop AS alloc_in_loop, f.recursion_in_loop AS recursion_in_loop, f.linear_scan_in_loop AS linear_scan_in_loop LIMIT 200"; }
58
+
59
+ # ── Runner + classifier ──
60
+
61
+ # gq_run <project> <cypher>
62
+ # Runs query_graph and echoes the validated raw engine JSON. Builds the JSON
63
+ # payload with jq so a quote/backslash in the query can never produce invalid
64
+ # JSON. Requires MEMORY_BIN (set by find_memory_bin) and jq. Returns:
65
+ # 0 valid JSON emitted on stdout
66
+ # 3 engine unavailable / no binary / non-JSON output (FAIL-LOUD: the caller
67
+ # must NOT treat this as an empty true-negative result)
68
+ gq_run() {
69
+ local project="$1" query="$2"
70
+ [[ -n "${MEMORY_BIN:-}" ]] || return 3
71
+ command -v jq >/dev/null 2>&1 || return 3
72
+ local payload res
73
+ payload="$(jq -n --arg p "$project" --arg q "$query" '{project:$p, query:$q}')" || return 3
74
+ res="$(memory_cli query_graph "$payload" 2>/dev/null || true)"
75
+ [[ -n "$res" ]] || return 3
76
+ printf '%s' "$res" | jq -e . >/dev/null 2>&1 || return 3
77
+ printf '%s' "$res"
78
+ }
79
+
80
+ # gq_rows_len <json> -> integer row count (0 on parse failure).
81
+ gq_rows_len() {
82
+ printf '%s' "$1" | jq -r '(.rows // []) | length' 2>/dev/null || printf '0'
83
+ }
84
+
85
+ # gq_symbol_status <project> <sym_esc> <result_json> -> ok | no-edges | no-match
86
+ # Fail-loud disambiguation (Guardrail 4): an empty result is only a true negative
87
+ # ("no-edges") when the node actually exists; otherwise it is "no-match". An
88
+ # existence probe runs only when the primary result is empty (hot path stays one
89
+ # query).
90
+ gq_symbol_status() {
91
+ local project="$1" sym="$2" result="$3"
92
+ if [[ "$(gq_rows_len "$result")" -gt 0 ]]; then
93
+ printf 'ok'; return
94
+ fi
95
+ local ex
96
+ ex="$(gq_run "$project" "$(gq_q_exists "$sym")" || true)"
97
+ if [[ -n "$ex" && "$(gq_rows_len "$ex")" -gt 0 ]]; then
98
+ printf 'no-edges'
99
+ else
100
+ printf 'no-match'
101
+ fi
102
+ }
@@ -15,8 +15,8 @@
15
15
  # Exit codes: 0 OK, 1 invocation error, 2 graph engine/data unavailable.
16
16
  set -euo pipefail
17
17
 
18
- # shellcheck source=_lib.sh
19
- source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
18
+ # shellcheck source=_graph_queries.sh
19
+ source "$(dirname "${BASH_SOURCE[0]}")/_graph_queries.sh"
20
20
 
21
21
  REPO="."
22
22
 
@@ -31,8 +31,10 @@ Flags:
31
31
  --repo DIR Repository root (default: cwd).
32
32
  --help Show this help.
33
33
 
34
- Output: JSON {cycles:[[a,b],[a,b,c]], source}. Fallback when the engine is
35
- unavailable: {"cycles":[],"source":"unavailable"}, exit 2.
34
+ Output: JSON {cycles:[[a,b],[a,b,c]], truncated, source}. `truncated` is true
35
+ when either cycle query hit its LIMIT (results are a sample, not exhaustive).
36
+ Fallback when the engine is unavailable: {"cycles":[],"source":"unavailable"},
37
+ exit 2.
36
38
  EOF
37
39
  }
38
40
 
@@ -60,20 +62,21 @@ command -v jq >/dev/null 2>&1 || unavailable
60
62
  PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
61
63
  [[ -n "$PROJECT" ]] || unavailable
62
64
 
63
- # 2-cycles: a -> b -> a (dedup with a.qualified_name < b.qualified_name).
64
- Q2="MATCH (a:Function)-[:CALLS]->(b:Function)-[:CALLS]->(a) WHERE a.qualified_name < b.qualified_name RETURN a.qualified_name AS a, b.qualified_name AS b LIMIT 100"
65
- # 3-cycles: a -> b -> c -> a.
66
- Q3="MATCH (a:Function)-[:CALLS]->(b:Function)-[:CALLS]->(c:Function)-[:CALLS]->(a) RETURN a.qualified_name AS a, b.qualified_name AS b, c.qualified_name AS c LIMIT 100"
67
-
68
- R2="$(memory_cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"$Q2\"}" || echo '{}')"
69
- R3="$(memory_cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"$Q3\"}" || echo '{}')"
65
+ # 2- and 3-node CALLS cycles. Cypher lives in _graph_queries.sh (label-agnostic;
66
+ # the Phase 0 fix code units are mostly :Method, and CALLS only connects
67
+ # callables). LIMIT 100 caps each, so results are a sample, not exhaustive.
68
+ R2="$(gq_run "$PROJECT" "$(gq_q_cycles2)" || echo '{}')"
69
+ R3="$(gq_run "$PROJECT" "$(gq_q_cycles3)" || echo '{}')"
70
70
 
71
71
  # Guard against empty/non-JSON engine output so --argjson never aborts the script.
72
72
  echo "$R2" | jq -e . >/dev/null 2>&1 || R2='{}'
73
73
  echo "$R3" | jq -e . >/dev/null 2>&1 || R3='{}'
74
74
 
75
75
  jq -n --argjson r2 "$R2" --argjson r3 "$R3" '
76
- {
77
- cycles: (((($r2.rows) // []) + (($r3.rows) // []))),
78
- source: "memory-graph"
79
- }'
76
+ ( ((($r2.rows) // []) | length) >= 100
77
+ or ((($r3.rows) // []) | length) >= 100 ) as $trunc
78
+ | {
79
+ cycles: (((($r2.rows) // []) + (($r3.rows) // []))),
80
+ truncated: $trunc,
81
+ source: "memory-graph"
82
+ }'
@@ -1,38 +1,54 @@
1
1
  #!/usr/bin/env bash
2
- # graph-callers.sh — enumerate direct callers of a function, from the knowledge graph.
2
+ # graph-callers.sh — enumerate callers of a function, from the knowledge graph.
3
3
  #
4
4
  # Replaces `graph --query --symbol <name> --mode callers`. Backed by the
5
- # codebase-memory-mcp engine via a single-hop CALLS openCypher pattern (the
6
- # dialect handles fixed-length patterns reliably).
5
+ # codebase-memory-mcp engine. All Cypher lives in _graph_queries.sh (single
6
+ # source of query truth); this entrypoint only parses args and shapes JSON.
7
7
  #
8
8
  # Usage:
9
9
  # scripts/tools/graph-callers.sh --repo DIR --symbol NAME
10
+ # [--transitive[=N]] [--prod-only] [--qualified]
10
11
  #
11
- # Output: JSON {symbol, callers:[{name,file}], source}.
12
+ # Output: JSON {symbol, callers:[{name,file[,hop]}], status, source}.
12
13
  # source = "memory-graph" | "unavailable"
14
+ # status = "ok" | "no-edges" | "no-match" | "unavailable"
15
+ # (fail-loud: distinguishes node-not-found from node-has-no-callers
16
+ # from engine-unavailable — never a bare [] that reads as a true
17
+ # negative).
13
18
  #
14
19
  # Exit codes: 0 OK, 1 invocation error, 2 graph engine unavailable.
15
20
  set -euo pipefail
16
21
 
17
- # shellcheck source=_lib.sh
18
- source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
22
+ # shellcheck source=_graph_queries.sh
23
+ source "$(dirname "${BASH_SOURCE[0]}")/_graph_queries.sh"
19
24
 
20
25
  REPO="."
21
26
  SYMBOL=""
27
+ TRANSITIVE=0
28
+ DEPTH=3
29
+ PROD_ONLY=0
30
+ QUALIFIED=0
22
31
 
23
32
  usage() {
24
33
  cat <<'EOF'
25
- graph-callers.sh — direct callers of a function.
34
+ graph-callers.sh — callers of a function from the knowledge graph.
26
35
 
27
36
  Usage:
28
- scripts/tools/graph-callers.sh --repo DIR --symbol NAME
37
+ scripts/tools/graph-callers.sh --repo DIR --symbol NAME [options]
29
38
 
30
39
  Flags:
31
- --repo DIR Repository root (default: cwd).
32
- --symbol NAME Function name to find callers of (required).
33
- --help Show this help.
40
+ --repo DIR Repository root (default: cwd).
41
+ --symbol NAME Function name to find callers of (required).
42
+ --transitive[=N] Transitive (upstream) callers via the trace_path expander,
43
+ depth N (default 3). Adds a `hop` field per caller.
44
+ --prod-only Best-effort exclude test/mock callers (is_test=false AND not
45
+ under tests/). is_test is partial in the engine, so this is a
46
+ heuristic, not a guarantee. Ignored with --transitive.
47
+ --qualified Match SYMBOL against qualified_name instead of name (use to
48
+ disambiguate same-named nodes). Ignored with --transitive.
49
+ --help Show this help.
34
50
 
35
- Output: JSON {symbol, callers, source}. Exit 2 when engine unavailable.
51
+ Output: JSON {symbol, callers, status, source}. Exit 2 when engine unavailable.
36
52
  EOF
37
53
  }
38
54
 
@@ -40,6 +56,10 @@ while [[ $# -gt 0 ]]; do
40
56
  case "$1" in
41
57
  --repo) REPO="$2"; shift 2;;
42
58
  --symbol) SYMBOL="$2"; shift 2;;
59
+ --transitive) TRANSITIVE=1; shift;;
60
+ --transitive=*) TRANSITIVE=1; DEPTH="${1#*=}"; shift;;
61
+ --prod-only) PROD_ONLY=1; shift;;
62
+ --qualified) QUALIFIED=1; shift;;
43
63
  --help|-h) usage; exit 0;;
44
64
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
45
65
  esac
@@ -47,13 +67,14 @@ done
47
67
 
48
68
  [[ -d "$REPO" ]] || { echo "ERROR: --repo '$REPO' is not a directory" >&2; exit 1; }
49
69
  [[ -n "$SYMBOL" ]] || { echo "ERROR: --symbol is required" >&2; usage >&2; exit 1; }
70
+ [[ "$DEPTH" =~ ^[0-9]+$ ]] || { echo "ERROR: --transitive depth must be a non-negative integer" >&2; exit 1; }
50
71
 
51
72
  REPO_ABS="$(cd "$REPO" && pwd)"
52
73
  SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
53
74
 
54
75
  unavailable() {
55
- jq -n --arg s "$SYMBOL" '{symbol:$s, callers:[], source:"unavailable"}' 2>/dev/null \
56
- || echo '{"callers":[],"source":"unavailable"}'
76
+ jq -n --arg s "$SYMBOL" '{symbol:$s, callers:[], status:"unavailable", source:"unavailable"}' 2>/dev/null \
77
+ || echo '{"callers":[],"status":"unavailable","source":"unavailable"}'
57
78
  exit 2
58
79
  }
59
80
 
@@ -63,12 +84,42 @@ command -v jq >/dev/null 2>&1 || unavailable
63
84
  PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
64
85
  [[ -n "$PROJECT" ]] || unavailable
65
86
 
66
- # Escape single quotes in the symbol for the Cypher string literal.
67
- SYM_ESC="${SYMBOL//\'/\\\'}"
68
- Q="MATCH (c)-[:CALLS]->(f:Function {name:'$SYM_ESC'}) RETURN c.name AS caller, c.file_path AS file LIMIT 200"
69
- RES="$(memory_cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"$Q\"}" || echo '{}')"
87
+ SYM_ESC="$(gq_escape "$SYMBOL")"
70
88
 
71
- echo "${RES:-{\}}" | jq --arg s "$SYMBOL" '
89
+ if [[ "$TRANSITIVE" -eq 1 ]]; then
90
+ # Transitive upstream callers via the trace_path depth-bounded expander.
91
+ # direction:"both" is the reliable form; we read its .callers array.
92
+ PAYLOAD="$(jq -n --arg p "$PROJECT" --arg f "$SYMBOL" --argjson d "$DEPTH" \
93
+ '{project:$p, function_name:$f, depth:$d, direction:"both"}')"
94
+ RES="$(memory_cli trace_path "$PAYLOAD" 2>/dev/null || true)"
95
+ echo "$RES" | jq -e . >/dev/null 2>&1 || unavailable
96
+ N="$(echo "$RES" | jq -r '(.callers // []) | length' 2>/dev/null || echo 0)"
97
+ if [[ "$N" -gt 0 ]]; then STATUS="ok"; else
98
+ EX="$(gq_run "$PROJECT" "$(gq_q_exists "$SYM_ESC")" || true)"
99
+ if [[ -n "$EX" && "$(gq_rows_len "$EX")" -gt 0 ]]; then STATUS="no-edges"; else STATUS="no-match"; fi
100
+ fi
101
+ echo "$RES" | jq --arg s "$SYMBOL" --arg st "$STATUS" '
102
+ {symbol:$s,
103
+ callers: [ (.callers // [])[] | {name:.name, file:(.qualified_name // ""), hop:(.hop // 1)} ],
104
+ status:$st, source:"memory-graph"}'
105
+ exit 0
106
+ fi
107
+
108
+ # Direct (single-hop) callers. Pick the builder by mode.
109
+ if [[ "$QUALIFIED" -eq 1 ]]; then
110
+ Q="$(gq_q_callers_qualified "$SYM_ESC")"
111
+ elif [[ "$PROD_ONLY" -eq 1 ]]; then
112
+ Q="$(gq_q_callers_prod "$SYM_ESC")"
113
+ else
114
+ Q="$(gq_q_callers "$SYM_ESC")"
115
+ fi
116
+
117
+ RES="$(gq_run "$PROJECT" "$Q" || true)"
118
+ [[ -n "$RES" ]] || unavailable
119
+
120
+ STATUS="$(gq_symbol_status "$PROJECT" "$SYM_ESC" "$RES")"
121
+
122
+ echo "$RES" | jq --arg s "$SYMBOL" --arg st "$STATUS" '
72
123
  {symbol:$s,
73
124
  callers: [ (.rows // [])[] | {name:.[0], file:.[1]} ],
74
- source:"memory-graph"}'
125
+ status:$st, source:"memory-graph"}'
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env bash
2
+ # graph-deps.sh — real file/module import graph from the knowledge graph (IMPORTS).
3
+ #
4
+ # graph-tooling-v2 Phase 3/4. The authoritative module-dependency graph, derived
5
+ # from actual IMPORTS edges — not the directory-stub `fan_in` heuristic. Feeds the
6
+ # architecture.md §9 dependency diagram (mermaid-from-graph.sh module-deps) and
7
+ # blast-radius reasoning.
8
+ #
9
+ # Self-imports (src == dst, an artifact of how some languages attribute a file's
10
+ # own symbols) are filtered out so the result is a true cross-file graph.
11
+ #
12
+ # Usage:
13
+ # scripts/tools/graph-deps.sh --repo DIR [--file PATH]
14
+ #
15
+ # Output: JSON {imports:[{src,dst}], total, truncated, source}. With --file, only
16
+ # edges whose src ends with PATH are kept (a file's outgoing dependencies).
17
+ #
18
+ # Exit codes: 0 OK, 1 invocation error, 2 graph engine unavailable.
19
+ set -euo pipefail
20
+
21
+ # shellcheck source=_graph_queries.sh
22
+ source "$(dirname "${BASH_SOURCE[0]}")/_graph_queries.sh"
23
+
24
+ REPO="."
25
+ FILE=""
26
+
27
+ usage() {
28
+ cat <<'EOF'
29
+ graph-deps.sh — real module/file import graph (IMPORTS edges).
30
+
31
+ Usage:
32
+ scripts/tools/graph-deps.sh --repo DIR [--file PATH]
33
+
34
+ Flags:
35
+ --repo DIR Repository root (default: cwd).
36
+ --file PATH Keep only edges whose source file ends with PATH.
37
+ --help Show this help.
38
+
39
+ Output: JSON {imports:[{src,dst}], total, truncated, source}. Self-imports are
40
+ filtered. Exit 2 when engine unavailable.
41
+ EOF
42
+ }
43
+
44
+ while [[ $# -gt 0 ]]; do
45
+ case "$1" in
46
+ --repo) REPO="$2"; shift 2;;
47
+ --file) FILE="$2"; shift 2;;
48
+ --help|-h) usage; exit 0;;
49
+ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
50
+ esac
51
+ done
52
+
53
+ [[ -d "$REPO" ]] || { echo "ERROR: --repo '$REPO' is not a directory" >&2; exit 1; }
54
+
55
+ REPO_ABS="$(cd "$REPO" && pwd)"
56
+ SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
57
+
58
+ unavailable() { echo '{"imports":[],"total":0,"source":"unavailable"}'; exit 2; }
59
+
60
+ find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
61
+ command -v jq >/dev/null 2>&1 || unavailable
62
+
63
+ PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
64
+ [[ -n "$PROJECT" ]] || unavailable
65
+
66
+ RES="$(gq_run "$PROJECT" "$(gq_q_imports)" || true)"
67
+ [[ -n "$RES" ]] || unavailable
68
+
69
+ echo "$RES" | jq --arg f "$FILE" '
70
+ [ (.rows // [])[]
71
+ | {src:.[0], dst:.[1]}
72
+ | select(.src != null and .dst != null and .src != .dst)
73
+ | select($f == "" or (.src | endswith($f))) ] as $e
74
+ | {imports: $e, total: ($e | length),
75
+ truncated: (((.rows // []) | length) >= 1000),
76
+ source:"memory-graph"}'
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # graph-errors.sh — error-propagation paths from the knowledge graph (RAISES/THROWS).
3
+ #
4
+ # graph-tooling-v2 Phase 3. Two modes:
5
+ # --symbol NAME what error types NAME raises/throws.
6
+ # --type NAME who raises/throws error type NAME (fail-closed audits: confirm
7
+ # every site that can emit a given error).
8
+ #
9
+ # Usage:
10
+ # scripts/tools/graph-errors.sh --repo DIR (--symbol NAME | --type NAME)
11
+ #
12
+ # Output (--symbol): {symbol, raises:[{error,qualified}], status, source}
13
+ # Output (--type): {type, raisers:[{symbol,file}], status, source}
14
+ #
15
+ # Exit codes: 0 OK, 1 invocation error, 2 graph engine unavailable.
16
+ set -euo pipefail
17
+
18
+ # shellcheck source=_graph_queries.sh
19
+ source "$(dirname "${BASH_SOURCE[0]}")/_graph_queries.sh"
20
+
21
+ REPO="."
22
+ SYMBOL=""
23
+ TYPE=""
24
+
25
+ usage() {
26
+ cat <<'EOF'
27
+ graph-errors.sh — error propagation (RAISES/THROWS edges).
28
+
29
+ Usage:
30
+ scripts/tools/graph-errors.sh --repo DIR (--symbol NAME | --type NAME)
31
+
32
+ Flags:
33
+ --repo DIR Repository root (default: cwd).
34
+ --symbol NAME Error types raised/thrown by NAME.
35
+ --type NAME Symbols that raise/throw error type NAME.
36
+ --help Show this help.
37
+
38
+ Output: --symbol → {symbol, raises, status, source}; --type → {type, raisers,
39
+ status, source}. Exit 2 when engine unavailable.
40
+ EOF
41
+ }
42
+
43
+ while [[ $# -gt 0 ]]; do
44
+ case "$1" in
45
+ --repo) REPO="$2"; shift 2;;
46
+ --symbol) SYMBOL="$2"; shift 2;;
47
+ --type) TYPE="$2"; shift 2;;
48
+ --help|-h) usage; exit 0;;
49
+ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
50
+ esac
51
+ done
52
+
53
+ [[ -d "$REPO" ]] || { echo "ERROR: --repo '$REPO' is not a directory" >&2; exit 1; }
54
+ if [[ -n "$SYMBOL" && -n "$TYPE" ]]; then
55
+ echo "ERROR: use either --symbol or --type, not both" >&2; exit 1
56
+ fi
57
+ [[ -n "$SYMBOL" || -n "$TYPE" ]] || { echo "ERROR: provide --symbol or --type" >&2; usage >&2; exit 1; }
58
+
59
+ REPO_ABS="$(cd "$REPO" && pwd)"
60
+ SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
61
+
62
+ unavailable() {
63
+ if [[ -n "$TYPE" ]]; then
64
+ jq -n --arg t "$TYPE" '{type:$t, raisers:[], status:"unavailable", source:"unavailable"}' 2>/dev/null \
65
+ || echo '{"raisers":[],"status":"unavailable","source":"unavailable"}'
66
+ else
67
+ jq -n --arg s "$SYMBOL" '{symbol:$s, raises:[], status:"unavailable", source:"unavailable"}' 2>/dev/null \
68
+ || echo '{"raises":[],"status":"unavailable","source":"unavailable"}'
69
+ fi
70
+ exit 2
71
+ }
72
+
73
+ find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
74
+ command -v jq >/dev/null 2>&1 || unavailable
75
+
76
+ PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
77
+ [[ -n "$PROJECT" ]] || unavailable
78
+
79
+ if [[ -n "$TYPE" ]]; then
80
+ SYM_ESC="$(gq_escape "$TYPE")"
81
+ RES="$(gq_run "$PROJECT" "$(gq_q_raisers "$SYM_ESC")" || true)"
82
+ [[ -n "$RES" ]] || unavailable
83
+ STATUS="$(gq_symbol_status "$PROJECT" "$SYM_ESC" "$RES")"
84
+ echo "$RES" | jq --arg t "$TYPE" --arg st "$STATUS" '
85
+ {type:$t,
86
+ raisers: [ (.rows // [])[] | {symbol:.[0], file:.[1]} ],
87
+ status:$st, source:"memory-graph"}'
88
+ else
89
+ SYM_ESC="$(gq_escape "$SYMBOL")"
90
+ RES="$(gq_run "$PROJECT" "$(gq_q_raises "$SYM_ESC")" || true)"
91
+ [[ -n "$RES" ]] || unavailable
92
+ STATUS="$(gq_symbol_status "$PROJECT" "$SYM_ESC" "$RES")"
93
+ echo "$RES" | jq --arg s "$SYMBOL" --arg st "$STATUS" '
94
+ {symbol:$s,
95
+ raises: [ (.rows // [])[] | {error:.[0], qualified:.[1]} ],
96
+ status:$st, source:"memory-graph"}'
97
+ fi
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bash
2
+ # graph-hierarchy.sh — class inheritance from the knowledge graph (INHERITS edges).
3
+ #
4
+ # graph-tooling-v2 Phase 3. Answers base/derived relationships and the impact of a
5
+ # base-class change.
6
+ # (default) all INHERITS edges (child → parent).
7
+ # --symbol NAME bases of class NAME (what NAME inherits from).
8
+ # --derived NAME subclasses of class NAME (who inherits from NAME — the blast
9
+ # radius of changing NAME).
10
+ #
11
+ # Usage:
12
+ # scripts/tools/graph-hierarchy.sh --repo DIR [--symbol NAME | --derived NAME]
13
+ #
14
+ # Output: JSON {edges:[{child,parent}], status, source}.
15
+ #
16
+ # Exit codes: 0 OK, 1 invocation error, 2 graph engine unavailable.
17
+ set -euo pipefail
18
+
19
+ # shellcheck source=_graph_queries.sh
20
+ source "$(dirname "${BASH_SOURCE[0]}")/_graph_queries.sh"
21
+
22
+ REPO="."
23
+ SYMBOL=""
24
+ DERIVED=""
25
+
26
+ usage() {
27
+ cat <<'EOF'
28
+ graph-hierarchy.sh — class inheritance (INHERITS edges).
29
+
30
+ Usage:
31
+ scripts/tools/graph-hierarchy.sh --repo DIR [--symbol NAME | --derived NAME]
32
+
33
+ Flags:
34
+ --repo DIR Repository root (default: cwd).
35
+ --symbol NAME Bases of class NAME (what it inherits from).
36
+ --derived NAME Subclasses of class NAME (impact of changing NAME).
37
+ --help Show this help.
38
+
39
+ Output: JSON {edges:[{child,parent}], status, source}. With no class filter,
40
+ emits the full hierarchy. Exit 2 when engine unavailable.
41
+ EOF
42
+ }
43
+
44
+ while [[ $# -gt 0 ]]; do
45
+ case "$1" in
46
+ --repo) REPO="$2"; shift 2;;
47
+ --symbol) SYMBOL="$2"; shift 2;;
48
+ --derived) DERIVED="$2"; shift 2;;
49
+ --help|-h) usage; exit 0;;
50
+ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
51
+ esac
52
+ done
53
+
54
+ [[ -d "$REPO" ]] || { echo "ERROR: --repo '$REPO' is not a directory" >&2; exit 1; }
55
+ if [[ -n "$SYMBOL" && -n "$DERIVED" ]]; then
56
+ echo "ERROR: use either --symbol or --derived, not both" >&2; exit 1
57
+ fi
58
+
59
+ REPO_ABS="$(cd "$REPO" && pwd)"
60
+ SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
61
+
62
+ unavailable() { echo '{"edges":[],"status":"unavailable","source":"unavailable"}'; exit 2; }
63
+
64
+ find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
65
+ command -v jq >/dev/null 2>&1 || unavailable
66
+
67
+ PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
68
+ [[ -n "$PROJECT" ]] || unavailable
69
+
70
+ if [[ -n "$SYMBOL" ]]; then
71
+ SYM_ESC="$(gq_escape "$SYMBOL")"; Q="$(gq_q_inherits_sym "$SYM_ESC")"; PROBE="$SYM_ESC"
72
+ elif [[ -n "$DERIVED" ]]; then
73
+ SYM_ESC="$(gq_escape "$DERIVED")"; Q="$(gq_q_derived_sym "$SYM_ESC")"; PROBE="$SYM_ESC"
74
+ else
75
+ Q="$(gq_q_inherits)"; PROBE=""
76
+ fi
77
+
78
+ RES="$(gq_run "$PROJECT" "$Q" || true)"
79
+ [[ -n "$RES" ]] || unavailable
80
+
81
+ if [[ -n "$PROBE" ]]; then
82
+ STATUS="$(gq_symbol_status "$PROJECT" "$PROBE" "$RES")"
83
+ else
84
+ STATUS="ok"
85
+ fi
86
+
87
+ echo "$RES" | jq --arg st "$STATUS" '
88
+ {edges: [ (.rows // [])[] | {child:.[0], parent:.[1]} ],
89
+ status:$st, source:"memory-graph"}'