@drafthq/draft 3.0.0 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/bin/README.md +4 -7
- package/cli/src/hosts/claude-code.js +4 -1
- package/cli/src/installer.js +12 -5
- package/core/shared/condensation.md +8 -8
- package/core/shared/draft-context-loading.md +5 -9
- package/core/shared/graph-query.md +170 -33
- package/core/shared/graph-usage-report.md +1 -1
- package/core/shared/pattern-learning.md +2 -2
- package/core/shared/red-flags.md +3 -3
- package/core/templates/ai-context.md +1 -1
- package/core/templates/architecture.md +3 -3
- package/integrations/agents/AGENTS.md +299 -145
- package/integrations/copilot/.github/copilot-instructions.md +299 -145
- package/package.json +2 -1
- package/scripts/lib.sh +11 -3
- package/scripts/tools/_graph_queries.sh +102 -0
- package/scripts/tools/adr-index.sh +2 -2
- package/scripts/tools/check-scope-conflicts.sh +2 -2
- package/scripts/tools/check-skill-line-caps.sh +2 -2
- package/scripts/tools/cycle-detect.sh +22 -15
- package/scripts/tools/diff-templates-vs-tracks.sh +2 -2
- package/scripts/tools/fix-whitespace.sh +15 -9
- package/scripts/tools/graph-arch.sh +72 -0
- package/scripts/tools/graph-callers.sh +71 -20
- package/scripts/tools/graph-deps.sh +76 -0
- package/scripts/tools/graph-errors.sh +97 -0
- package/scripts/tools/graph-hierarchy.sh +89 -0
- package/scripts/tools/graph-impact.sh +1 -0
- package/scripts/tools/graph-init.sh +3 -3
- package/scripts/tools/graph-query.sh +124 -0
- package/scripts/tools/graph-risk.sh +81 -0
- package/scripts/tools/graph-search.sh +84 -0
- package/scripts/tools/graph-snapshot.sh +63 -50
- package/scripts/tools/graph-snippet.sh +92 -0
- package/scripts/tools/graph-tests.sh +112 -0
- package/scripts/tools/graph-traces.sh +83 -0
- package/scripts/tools/hotspot-rank.sh +44 -15
- package/scripts/tools/manage-symlinks.sh +1 -1
- package/scripts/tools/mermaid-from-graph.sh +31 -10
- package/scripts/tools/parse-reports.sh +1 -1
- package/scripts/tools/verify-doc-anchors.sh +2 -2
- package/scripts/tools/verify-graph-binary.sh +1 -1
- package/skills/GRAPH.md +2 -2
- package/skills/bughunt/SKILL.md +1 -1
- package/skills/debug/SKILL.md +3 -3
- package/skills/decompose/SKILL.md +5 -5
- package/skills/deep-review/SKILL.md +2 -2
- package/skills/deploy-checklist/SKILL.md +2 -2
- package/skills/graph/SKILL.md +1 -1
- package/skills/implement/SKILL.md +1 -1
- package/skills/init/SKILL.md +62 -42
- package/skills/init/references/architecture-spec.md +12 -11
- package/skills/learn/SKILL.md +5 -5
- package/skills/quick-review/SKILL.md +3 -3
- package/skills/review/SKILL.md +7 -7
- package/skills/tech-debt/SKILL.md +2 -2
- package/scripts/tools/okf-bundle.sh +0 -141
- package/scripts/tools/okf-check.sh +0 -137
- package/scripts/tools/okf-emit.sh +0 -161
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drafthq/draft",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Context-Driven Development for AI coding agents — install Draft into Claude Code, Cursor, Codex, or opencode.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"draft": "cli/bin/draft.js"
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
26
|
"test": "bash tests/test-cli.sh",
|
|
27
|
+
"version": "bash scripts/sync-version.sh && git add .claude-plugin/plugin.json .claude-plugin/marketplace.json web/index.html",
|
|
27
28
|
"prepublishOnly": "bash scripts/build-integrations.sh"
|
|
28
29
|
},
|
|
29
30
|
"repository": {
|
package/scripts/lib.sh
CHANGED
|
@@ -158,11 +158,19 @@ TOOLS=(
|
|
|
158
158
|
"mermaid-from-graph.sh"
|
|
159
159
|
"graph-snapshot.sh"
|
|
160
160
|
"graph-init.sh"
|
|
161
|
-
"okf-emit.sh"
|
|
162
|
-
"okf-bundle.sh"
|
|
163
|
-
"okf-check.sh"
|
|
164
161
|
"graph-impact.sh"
|
|
165
162
|
"graph-callers.sh"
|
|
163
|
+
"graph-arch.sh"
|
|
164
|
+
# graph-tooling-v2: generic passthrough + purpose-built capability wrappers
|
|
165
|
+
"graph-query.sh"
|
|
166
|
+
"graph-snippet.sh"
|
|
167
|
+
"graph-search.sh"
|
|
168
|
+
"graph-tests.sh"
|
|
169
|
+
"graph-deps.sh"
|
|
170
|
+
"graph-hierarchy.sh"
|
|
171
|
+
"graph-errors.sh"
|
|
172
|
+
"graph-risk.sh"
|
|
173
|
+
"graph-traces.sh"
|
|
166
174
|
"validate-frontmatter.sh"
|
|
167
175
|
# Foundations hygiene/verification tools
|
|
168
176
|
"check-graph-usage-report.sh"
|
|
@@ -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
|
+
}
|
|
@@ -84,7 +84,7 @@ while IFS= read -r -d '' file; do
|
|
|
84
84
|
|
|
85
85
|
# If no title in frontmatter, fallback to first H1.
|
|
86
86
|
if [[ -z "$title" ]]; then
|
|
87
|
-
title="$(grep -m1 '^# ' "$file" 2>/dev/null | sed 's
|
|
87
|
+
title="$(grep -m1 '^# ' "$file" 2>/dev/null | sed 's/^#[[:space:]]*//' || true)"
|
|
88
88
|
fi
|
|
89
89
|
|
|
90
90
|
tracks=()
|
|
@@ -108,7 +108,7 @@ while IFS= read -r -d '' file; do
|
|
|
108
108
|
"$(json_escape "$status")" \
|
|
109
109
|
"$(json_escape "$file")" \
|
|
110
110
|
"$tr_json"
|
|
111
|
-
done < <(find "$ROOT" -maxdepth 2 -type f -name '*.md'
|
|
111
|
+
done < <(find "$ROOT" -maxdepth 2 -type f -name '*.md' 2>/dev/null | sort | tr '\n' '\0')
|
|
112
112
|
|
|
113
113
|
if $first; then
|
|
114
114
|
printf ']}\n'
|
|
@@ -112,7 +112,7 @@ emit() {
|
|
|
112
112
|
if ((EMIT_JSON)); then
|
|
113
113
|
printf '{"conflict_count": %d, "conflicts": [\n' "$conflict_count"
|
|
114
114
|
local first=1 c track kind detail
|
|
115
|
-
for c in "${conflicts[@]}"; do
|
|
115
|
+
for c in ${conflicts[@]+"${conflicts[@]}"}; do
|
|
116
116
|
IFS='|' read -r track kind detail <<< "$c"
|
|
117
117
|
if ((first)); then first=0; else printf ',\n'; fi
|
|
118
118
|
printf ' {"track":"%s","kind":"%s","detail":"%s"}' \
|
|
@@ -127,7 +127,7 @@ emit() {
|
|
|
127
127
|
printf 'SCOPE: %d conflict(s) across %d track(s).\n' \
|
|
128
128
|
"$conflict_count" "${#TRACK_PATHS[@]}" >&2
|
|
129
129
|
local c track kind detail
|
|
130
|
-
for c in "${conflicts[@]}"; do
|
|
130
|
+
for c in ${conflicts[@]+"${conflicts[@]}"}; do
|
|
131
131
|
IFS='|' read -r track kind detail <<< "$c"
|
|
132
132
|
printf ' [%s] %s — %s\n' "$kind" "$track" "$detail" >&2
|
|
133
133
|
done
|
|
@@ -78,7 +78,7 @@ emit() {
|
|
|
78
78
|
printf '{"over_cap_count": %d, "global_cap": %d, "findings": [\n' \
|
|
79
79
|
"$over_count" "$GLOBAL_CAP"
|
|
80
80
|
local first=1 v name lines cap
|
|
81
|
-
for v in "${findings[@]}"; do
|
|
81
|
+
for v in ${findings[@]+"${findings[@]}"}; do
|
|
82
82
|
IFS='|' read -r name lines cap <<< "$v"
|
|
83
83
|
if ((first)); then first=0; else printf ',\n'; fi
|
|
84
84
|
printf ' {"skill":"%s","lines":%d,"cap":%d}' \
|
|
@@ -100,7 +100,7 @@ emit() {
|
|
|
100
100
|
"$over_count" "$mode"
|
|
101
101
|
fi
|
|
102
102
|
local v name lines cap
|
|
103
|
-
for v in "${findings[@]}"; do
|
|
103
|
+
for v in ${findings[@]+"${findings[@]}"}; do
|
|
104
104
|
IFS='|' read -r name lines cap <<< "$v"
|
|
105
105
|
if ((ENFORCE)); then
|
|
106
106
|
printf ' %s: %d lines (cap %d)\n' "$name" "$lines" "$cap" >&2
|
|
@@ -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=
|
|
19
|
-
source "$(dirname "${BASH_SOURCE[0]}")/
|
|
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}.
|
|
35
|
-
|
|
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,16 +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-
|
|
64
|
-
|
|
65
|
-
#
|
|
66
|
-
|
|
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 '{}')"
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
# Guard against empty/non-JSON engine output so --argjson never aborts the script.
|
|
72
|
+
echo "$R2" | jq -e . >/dev/null 2>&1 || R2='{}'
|
|
73
|
+
echo "$R3" | jq -e . >/dev/null 2>&1 || R3='{}'
|
|
70
74
|
|
|
71
|
-
jq -n --argjson r2 "$
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
jq -n --argjson r2 "$R2" --argjson r3 "$R3" '
|
|
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
|
+
}'
|
|
@@ -146,7 +146,7 @@ emit_records() {
|
|
|
146
146
|
if ((EMIT_JSON)); then
|
|
147
147
|
printf '{"drift_count": %d, "records": [\n' "$drift_count"
|
|
148
148
|
local first=1
|
|
149
|
-
for r in "${drift_records[@]}"; do
|
|
149
|
+
for r in ${drift_records[@]+"${drift_records[@]}"}; do
|
|
150
150
|
local track kind detail
|
|
151
151
|
IFS='|' read -r track kind detail <<< "$r"
|
|
152
152
|
if ((first)); then first=0; else printf ',\n'; fi
|
|
@@ -163,7 +163,7 @@ emit_records() {
|
|
|
163
163
|
printf 'DRIFT: %d defect(s) across %d track(s).\n' \
|
|
164
164
|
"$drift_count" "${#TRACK_PATHS[@]}" >&2
|
|
165
165
|
local r track kind detail
|
|
166
|
-
for r in "${drift_records[@]}"; do
|
|
166
|
+
for r in ${drift_records[@]+"${drift_records[@]}"}; do
|
|
167
167
|
IFS='|' read -r track kind detail <<< "$r"
|
|
168
168
|
printf ' [%s] %s — %s\n' "$kind" "$track" "$detail" >&2
|
|
169
169
|
done
|
|
@@ -54,6 +54,11 @@ fix_file() {
|
|
|
54
54
|
return 2
|
|
55
55
|
fi
|
|
56
56
|
|
|
57
|
+
# Empty file: nothing to normalize (avoid writing a spurious newline).
|
|
58
|
+
if [[ ! -s "$file" ]]; then
|
|
59
|
+
return 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
57
62
|
local original
|
|
58
63
|
original="$(cat "$file")"
|
|
59
64
|
|
|
@@ -66,18 +71,19 @@ fix_file() {
|
|
|
66
71
|
| sed -e :a -e '/^\n*$/{$d;N;ba}'
|
|
67
72
|
)"$'\n'
|
|
68
73
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
# Compare the bytes we would write against the file on disk — NOT the
|
|
75
|
+
# command-substitution copies (which strip then re-add the trailing newline,
|
|
76
|
+
# so they could never compare equal). This keeps fix_file idempotent.
|
|
73
77
|
local _tmp
|
|
74
78
|
_tmp="$(mktemp "${file}.XXXXXX")"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
printf '%s' "$fixed" > "$_tmp" || { rm -f "$_tmp"; return 2; }
|
|
80
|
+
|
|
81
|
+
if cmp -s "$_tmp" "$file"; then
|
|
78
82
|
rm -f "$_tmp"
|
|
79
|
-
return
|
|
83
|
+
return 1 # already clean — no change on disk
|
|
80
84
|
fi
|
|
85
|
+
|
|
86
|
+
mv -f "$_tmp" "$file"
|
|
81
87
|
return 0
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -109,7 +115,7 @@ case "$1" in
|
|
|
109
115
|
fi
|
|
110
116
|
while IFS= read -r -d '' f; do
|
|
111
117
|
TARGETS+=("$f")
|
|
112
|
-
done < <(find "$TRACK_DIR" -maxdepth 1 -name "*.md"
|
|
118
|
+
done < <(find "$TRACK_DIR" -maxdepth 1 -name "*.md" | sort | tr '\n' '\0')
|
|
113
119
|
;;
|
|
114
120
|
--draft)
|
|
115
121
|
REPO_ROOT="${2:-$(pwd)}"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# graph-arch.sh — emit the architecture view from the knowledge graph.
|
|
3
|
+
#
|
|
4
|
+
# The engine-only replacement for the old committed architecture.json. Resolves
|
|
5
|
+
# the codebase-memory-mcp engine, indexes the repo on demand, auto-resolves the
|
|
6
|
+
# project, and prints the full get_architecture(all) JSON to stdout — node labels,
|
|
7
|
+
# edge types, languages, packages (fan-in/out), entry points, routes, hotspots,
|
|
8
|
+
# boundaries, layers, clusters, file tree. Pipe to jq to slice the field you need:
|
|
9
|
+
#
|
|
10
|
+
# scripts/tools/graph-arch.sh --repo . | jq '.packages'
|
|
11
|
+
# scripts/tools/graph-arch.sh --repo . | jq '.routes'
|
|
12
|
+
#
|
|
13
|
+
# The engine binary is usually NOT on $PATH; this wrapper resolves it (via
|
|
14
|
+
# _lib.sh:find_memory_bin) so callers never invoke `codebase-memory-mcp` directly.
|
|
15
|
+
#
|
|
16
|
+
# Usage: scripts/tools/graph-arch.sh [--repo DIR]
|
|
17
|
+
# Output: the architecture JSON object on success; {"source":"unavailable"} on failure.
|
|
18
|
+
# Exit codes: 0 OK, 1 invocation error, 2 graph engine/data unavailable.
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
# shellcheck source=_lib.sh
|
|
22
|
+
source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
|
|
23
|
+
|
|
24
|
+
REPO="."
|
|
25
|
+
|
|
26
|
+
usage() {
|
|
27
|
+
cat <<'EOF'
|
|
28
|
+
graph-arch.sh — architecture view (packages, routes, layers, hotspots) from the graph.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
scripts/tools/graph-arch.sh [--repo DIR]
|
|
32
|
+
|
|
33
|
+
Flags:
|
|
34
|
+
--repo DIR Repository root (default: cwd).
|
|
35
|
+
--help Show this help.
|
|
36
|
+
|
|
37
|
+
Output: the get_architecture(all) JSON object on success. Pipe to jq to slice it
|
|
38
|
+
(`| jq '.packages'`, `| jq '.routes'`, …). Emits {"source":"unavailable"} and exits 2
|
|
39
|
+
when the graph engine is unavailable.
|
|
40
|
+
EOF
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
while [[ $# -gt 0 ]]; do
|
|
44
|
+
case "$1" in
|
|
45
|
+
--repo) REPO="$2"; shift 2;;
|
|
46
|
+
--help|-h) usage; exit 0;;
|
|
47
|
+
*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
|
|
48
|
+
esac
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
if [[ ! -d "$REPO" ]]; then
|
|
52
|
+
echo "ERROR: --repo '$REPO' is not a directory" >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
REPO_ABS="$(cd "$REPO" && pwd)"
|
|
57
|
+
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
58
|
+
|
|
59
|
+
unavailable() { echo '{"source":"unavailable"}'; exit 2; }
|
|
60
|
+
|
|
61
|
+
find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
|
|
62
|
+
command -v jq >/dev/null 2>&1 || unavailable
|
|
63
|
+
|
|
64
|
+
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
|
|
65
|
+
[[ -n "$PROJECT" ]] || unavailable
|
|
66
|
+
|
|
67
|
+
ARCH_JSON="$(memory_cli get_architecture "{\"project\":\"$PROJECT\",\"aspects\":[\"all\"]}" || true)"
|
|
68
|
+
[[ -n "$ARCH_JSON" ]] || unavailable
|
|
69
|
+
|
|
70
|
+
# Validate it parses and looks like an architecture object before emitting.
|
|
71
|
+
echo "$ARCH_JSON" | jq -e '.total_nodes != null' >/dev/null 2>&1 || unavailable
|
|
72
|
+
echo "$ARCH_JSON" | jq '.'
|
|
@@ -1,38 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# graph-callers.sh — enumerate
|
|
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
|
|
6
|
-
#
|
|
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=
|
|
18
|
-
source "$(dirname "${BASH_SOURCE[0]}")/
|
|
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 —
|
|
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
|
|
32
|
-
--symbol NAME
|
|
33
|
-
--
|
|
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
|
-
|
|
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
|
-
|
|
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"}'
|