@drafthq/draft 4.0.0 → 5.0.1

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.
@@ -1,24 +1,28 @@
1
1
  #!/usr/bin/env bash
2
2
  # graph-impact.sh — blast radius for a file or symbol, from the knowledge graph.
3
3
  #
4
- # Replaces `graph --query --file <path> --mode impact`. Backed by the
5
- # codebase-memory-mcp engine: combines detect_changes (git-diff impacted
6
- # symbols, when querying the working tree) with trace_path callers for a named
7
- # function (transitive upstream dependents).
4
+ # Everything that depends on the target, up to --depth CALLS hops: callers of the
5
+ # symbol, or callers of anything defined in the file plus the files that import
6
+ # it. Callers inside the target file are not downstream and are skipped.
8
7
  #
9
8
  # Usage:
10
9
  # scripts/tools/graph-impact.sh --repo DIR (--file PATH | --symbol NAME) [--depth N]
11
10
  #
12
- # Output: JSON {target, kind, impacted:[{name,file,qualified,hop}], source}.
13
- # `file` is always a path (empty when the engine carries none); the qualified
14
- # name has its own field rather than being emitted as if it were a path.
11
+ # Output: JSON {target, kind, impacted:[{name,file,qualified,hop}], downstream_files,
12
+ # affected_modules, max_depth, by_category:{code,test}, status, truncated, source}.
13
+ # `impacted` lists each dependent once at its nearest hop, capped at 200; the
14
+ # aggregates always cover the full set. `truncated` is true when the list was
15
+ # capped or the engine row limit was hit. A module is a file's top-level path
16
+ # segment ("." for root files), as in classify-files.sh.
17
+ # status = ok | no-edges (target known, nothing depends on it) | no-match
18
+ # (target unknown to the graph)
15
19
  # source = "memory-graph" | "unavailable"
16
20
  #
17
21
  # Exit codes: 0 OK, 1 invocation error, 2 graph engine unavailable.
18
22
  set -euo pipefail
19
23
 
20
- # shellcheck source=_lib.sh
21
- source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
24
+ # shellcheck source=_graph_queries.sh
25
+ source "$(dirname "${BASH_SOURCE[0]}")/_graph_queries.sh"
22
26
 
23
27
  REPO="."
24
28
  FILE=""
@@ -34,12 +38,13 @@ Usage:
34
38
 
35
39
  Flags:
36
40
  --repo DIR Repository root (default: cwd).
37
- --file PATH Size impact of a changed file (uses git working-tree diff).
38
- --symbol NAME Transitive callers of a function (default depth 3).
39
- --depth N Caller traversal depth for --symbol (default: 3).
41
+ --file PATH Dependents of a file (repo-relative, ./-prefixed, or absolute).
42
+ --symbol NAME Dependents (transitive callers) of a function.
43
+ --depth N Caller traversal depth (default: 3).
40
44
  --help Show this help.
41
45
 
42
- Output: JSON {target, kind, impacted, source}. Exit 2 when engine unavailable.
46
+ Output: JSON {target, kind, impacted, downstream_files, affected_modules,
47
+ max_depth, by_category, status, truncated, source}. Exit 2 when engine unavailable.
43
48
  EOF
44
49
  }
45
50
 
@@ -70,29 +75,50 @@ if [[ -n "$SYMBOL" ]]; then TARGET="$SYMBOL"; KIND="symbol"; else TARGET="$FILE"
70
75
  graph_bootstrap "$REPO" || unavailable "$TARGET" "$KIND"
71
76
 
72
77
  if [[ -n "$SYMBOL" ]]; then
73
- # direction:"both" is the reliable form (the "callers" value returns empty in this engine);
74
- # we read the .callers array from it. Payload built with jq so a quote in
75
- # --symbol can never corrupt the JSON; an engine failure is unavailable,
76
- # never a fabricated empty-impact success.
77
- PAYLOAD="$(jq -n --arg p "$PROJECT" --arg f "$SYMBOL" --argjson d "$DEPTH" \
78
- '{project:$p, function_name:$f, depth:$d, direction:"both"}')"
79
- RES="$(memory_cli trace_path "$PAYLOAD" 2>/dev/null || true)"
80
- echo "$RES" | jq -e 'has("callers") and (.callers | type == "array")' >/dev/null 2>&1 \
81
- || unavailable "$TARGET" "$KIND"
82
- echo "$RES" | jq --arg t "$TARGET" '
83
- {target:$t, kind:"symbol",
84
- impacted: [ (.callers // [])[] | {name:.name, file:(.file_path // ""), qualified:(.qualified_name // ""), hop:(.hop // 1)} ],
85
- source:"memory-graph"}'
78
+ T_ESC="$(gq_escape "$SYMBOL")"
79
+ DEPENDENTS=gq_q_dependents_symbol; EXISTS=gq_q_exists
86
80
  else
87
- # File impact: detect_changes maps the working-tree diff to impacted symbols.
88
- PAYLOAD="$(jq -n --arg p "$PROJECT" '{project:$p}')"
89
- RES="$(memory_cli detect_changes "$PAYLOAD" 2>/dev/null || true)"
90
- echo "$RES" | jq -e 'has("impacted_symbols") and (.impacted_symbols | type == "array")' >/dev/null 2>&1 \
91
- || unavailable "$TARGET" "$KIND"
92
- echo "$RES" | jq --arg t "$TARGET" '
93
- {target:$t, kind:"file",
94
- impacted: [ (.impacted_symbols // [])[]
95
- | select((.file // "") | endswith($t) or (. == $t))
96
- | {name:.name, file:(.file // ""), hop:1} ],
97
- source:"memory-graph"}'
81
+ # The graph keys files by repo-relative path.
82
+ REL="${FILE#"$REPO_ABS"/}"; REL="${REL#./}"
83
+ T_ESC="$(gq_escape "$REL")"
84
+ DEPENDENTS=gq_q_dependents_file; EXISTS=gq_q_file_exists
98
85
  fi
86
+
87
+ # Accumulate {q,name,file,test,hop} JSONL in a temp file: the row sets of a hot
88
+ # target can exceed argv limits.
89
+ ROWS="$(mktemp)"
90
+ trap 'rm -f "$ROWS"' EXIT
91
+ TRUNC=false
92
+ for ((k = 1; k <= DEPTH; k++)); do
93
+ R="$(gq_run "$PROJECT" "$("$DEPENDENTS" "$T_ESC" "$k")")" || unavailable "$TARGET" "$KIND"
94
+ [[ "$(gq_rows_len "$R")" -lt "$GQ_DEP_LIMIT" ]] || TRUNC=true
95
+ jq -c --argjson k "$k" '.rows[] | {q:(.[0] // ""), name:(.[1] // ""), file:(.[2] // ""),
96
+ test:((.[3] | tostring) == "true"), hop:$k}' <<< "$R" >> "$ROWS"
97
+ done
98
+ if [[ -n "$FILE" ]]; then
99
+ R="$(gq_run "$PROJECT" "$(gq_q_importers "$T_ESC")")" || unavailable "$TARGET" "$KIND"
100
+ [[ "$(gq_rows_len "$R")" -lt "$GQ_DEP_LIMIT" ]] || TRUNC=true
101
+ jq -c '.rows[] | {q:"", name:.[0], file:.[0], test:false, hop:1}' <<< "$R" >> "$ROWS"
102
+ fi
103
+
104
+ # Nothing depends on it: a true negative only if the graph knows the target.
105
+ STATUS=ok
106
+ if [[ ! -s "$ROWS" ]]; then
107
+ EX="$(gq_run "$PROJECT" "$("$EXISTS" "$T_ESC")")" || unavailable "$TARGET" "$KIND"
108
+ if [[ "$(gq_rows_len "$EX")" -gt 0 ]]; then STATUS=no-edges; else STATUS=no-match; fi
109
+ fi
110
+
111
+ jq -s --arg t "$TARGET" --arg kind "$KIND" --arg status "$STATUS" --argjson trunc "$TRUNC" '
112
+ (group_by([.q, .file]) | map(min_by(.hop))) as $u
113
+ | ($u | map(.file) | map(select(. != "")) | unique) as $files
114
+ | ($u | map(select(.test) | .file) | unique) as $tests
115
+ | {target:$t, kind:$kind,
116
+ impacted: ($u | sort_by(.hop, .file, .name) | .[:200]
117
+ | map({name, file, qualified:.q, hop})),
118
+ downstream_files: $files,
119
+ affected_modules: ($files | map(if test("/") then split("/")[0] else "." end) | unique),
120
+ max_depth: ($u | map(.hop) | max // 0),
121
+ by_category: {code: ($files - $tests | length), test: ($tests | length)},
122
+ status: $status,
123
+ truncated: ($trunc or ($u | length) > 200),
124
+ source: "memory-graph"}' "$ROWS"
@@ -62,7 +62,7 @@ while [[ $# -gt 0 ]]; do
62
62
  done
63
63
 
64
64
  [[ -d "$SCOPE" ]] || { echo "ERROR: --scope '$SCOPE' is not a directory" >&2; exit 1; }
65
- SCOPE_ABS="$(cd "$SCOPE" && pwd)"
65
+ SCOPE_ABS="$(cd "$SCOPE" && pwd -P)"
66
66
  SELF_REPO="$(cd "$TOOLS_DIR/../.." && pwd)"
67
67
 
68
68
  # --- Resolve ROOT (bounded by the git toplevel; never escapes the repo) ---
@@ -127,22 +127,17 @@ write_root_link() {
127
127
  local status="$1"
128
128
  local mod_graph="$SCOPE_ABS/draft/graph"
129
129
  mkdir -p "$mod_graph"
130
- local rel root_project="unknown" root_commit ts schema="$ROOT_ABS/draft/graph/schema.yaml"
130
+ local rel root_commit ts
131
131
  rel="$(root_link_relpath)"
132
- if [[ -f "$schema" ]]; then
133
- root_project="$(grep -m1 '^project:' "$schema" 2>/dev/null | sed 's/^project:[[:space:]]*//; s/^"//; s/"$//' || true)"
134
- [[ -n "$root_project" ]] || root_project="unknown"
135
- fi
136
132
  root_commit="$(git -C "$ROOT_ABS" rev-parse --verify --quiet HEAD 2>/dev/null || echo none)"
137
133
  ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
138
- # Every interpolated value goes through json_escape: a repo path or engine
139
- # project name carrying a quote or backslash would otherwise emit a
140
- # root-link.json that no consumer can parse.
134
+ # Every interpolated value goes through json_escape: a repo path carrying a
135
+ # quote or backslash would otherwise emit a root-link.json that no consumer
136
+ # can parse.
141
137
  cat > "$mod_graph/root-link.json" <<EOF
142
138
  {
143
139
  "root_graph": "$(json_escape "$rel")",
144
140
  "root_abs": "$(json_escape "$ROOT_ABS/draft/graph")",
145
- "root_project": "$(json_escape "${root_project:-unknown}")",
146
141
  "root_commit": "$(json_escape "$root_commit")",
147
142
  "status": "$(json_escape "$status")",
148
143
  "linked_at": "$ts",
@@ -47,7 +47,7 @@ while [[ $# -gt 0 ]]; do
47
47
  done
48
48
  REPO="${REPO:-.}"
49
49
  [[ -d "$REPO" ]] || { echo "ERROR: '$REPO' is not a directory" >&2; exit 2; }
50
- REPO_ABS="$(cd "$REPO" && pwd)"
50
+ REPO_ABS="$(cd "$REPO" && pwd -P)"
51
51
 
52
52
  # --- formatting (color only on a tty, and never in --json) ---
53
53
  if [[ -t 1 && "$JSON_MODE" -eq 0 ]]; then B=$'\e[1m'; G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; D=$'\e[0m'; else B=""; G=""; Y=""; R=""; D=""; fi
@@ -168,6 +168,10 @@ if find_memory_bin "$REPO_ABS" "$SELF_REPO"; then
168
168
  ENGINE_FOUND=true
169
169
  VER="$("$ENGINE" --version 2>/dev/null | head -1 || echo '?')"
170
170
  ok "Engine: $ENGINE ($VER)"
171
+ PINNED="$(sed -n 's/^DEFAULT_VERSION="v\{0,1\}\([^"]*\)".*/\1/p' "$SELF_REPO/scripts/fetch-memory-engine.sh" 2>/dev/null | head -1)"
172
+ if [[ -n "$PINNED" && "${VER##* }" != "$PINNED" ]]; then
173
+ warn "Engine version ${VER##* } differs from the pinned $PINNED the graph tools are verified against — scripts/fetch-memory-engine.sh --force"
174
+ fi
171
175
  # Numeric-only: the value is emitted bare into the --json report, so a
172
176
  # non-numeric field (engine output format drift) would produce invalid JSON.
173
177
  LIMIT="$("$ENGINE" config list 2>/dev/null | awk '/auto_index_limit/{print $3}' || true)"
@@ -255,8 +259,7 @@ cat <<EOF
255
259
 
256
260
  Next step (when ready, from the git root):
257
261
  scripts/tools/graph-init.sh --scope . --json & # or: /draft:init --graph-only
258
- ${ENGINE:-codebase-memory-mcp} cli list_projects '{}'
259
- ${ENGINE:-codebase-memory-mcp} cli index_status '{"project":"<name>"}'
262
+ scripts/tools/graph-query.sh --repo . --tool index_status
260
263
  EOF
261
264
  hr
262
265
  exit "$VEXIT"
@@ -12,11 +12,12 @@
12
12
  # a `query` field in --json is scanned for write
13
13
  # verbs exactly like --cypher.
14
14
  #
15
- # Dialect limits (engine v0.8.x — see _graph_queries.sh for the full list):
16
- # SAFE : fixed-length patterns, `=`, `<`, `STARTS WITH`, `NOT x STARTS WITH`,
17
- # `AND`, `OR`, rel-type alternation `[:A|B]`, `count(x)`.
18
- # UNSAFE : coalesce(), `<>`/`!=`/`<=`/`>=`, NOT EXISTS(...), NOT (pattern),
19
- # WITH-grouping aggregation, multi-pattern joins.
15
+ # Dialect limits (engine v0.9.0 — see _graph_queries.sh for the full list):
16
+ # SAFE : comparisons against a literal, `STARTS WITH`, `AND`/`OR`,
17
+ # variable-length `[:R*1..3]`, rel-type alternation `[:A|B]`,
18
+ # `coalesce()`, `DISTINCT`, `count(...)`, WITH-aggregation.
19
+ # UNSAFE : property-to-property comparison, NOT EXISTS(...), NOT (pattern),
20
+ # path variables, multi-pattern joins. LIMIT applies before DISTINCT.
20
21
  # Passthrough returns the engine's raw error, not a silent empty result.
21
22
  #
22
23
  # Usage:
@@ -52,8 +53,8 @@ Flags:
52
53
  `query` field is write-verb checked like --cypher.
53
54
  --help Show this help.
54
55
 
55
- Dialect: avoid coalesce(), <>, NOT EXISTS, NOT(pattern), WITH-aggregation,
56
- multi-pattern joins. Use =, <, STARTS WITH, AND/OR, [:A|B] alternation.
56
+ Dialect: avoid property-to-property comparison, NOT EXISTS, NOT(pattern), path
57
+ variables, multi-pattern joins. LIMIT applies before DISTINCT.
57
58
 
58
59
  Output: raw engine JSON on success; {"source":"unavailable"} (exit 2) when the
59
60
  engine is unavailable; exit 1 on invocation error or a rejected write verb.
@@ -11,7 +11,7 @@
11
11
  # truth; the engine is the structural index over it.
12
12
  #
13
13
  # Writes one file under <repo>/draft/graph/:
14
- # schema.yaml engine + project metadata + index counts. Its presence is the
14
+ # schema.yaml engine metadata + index counts. Its presence is the
15
15
  # GATE that tells skills the graph engine is wired for this repo
16
16
  # (see core/shared/graph-query.md Pre-Check). It carries no graph
17
17
  # data — every structural query goes to the live engine.
@@ -61,30 +61,18 @@ done
61
61
 
62
62
  [[ -d "$REPO" ]] || { echo "ERROR: --repo '$REPO' is not a directory" >&2; exit 1; }
63
63
 
64
- REPO_ABS="$(cd "$REPO" && pwd)"
64
+ REPO_ABS="$(cd "$REPO" && pwd -P)"
65
65
  SELF_REPO="$(cd "$TOOLS_DIR/../.." && pwd)"
66
66
  OUT="${OUT_DIR:-$REPO_ABS/draft/graph}"
67
67
 
68
68
  find_memory_bin "$REPO_ABS" "$SELF_REPO" || { echo "graph engine unavailable — nothing written" >&2; exit 2; }
69
69
  command -v jq >/dev/null 2>&1 || { echo "jq required" >&2; exit 2; }
70
70
 
71
- # Index on demand; this is the valuable side-effect it ensures the engine holds
72
- # a current index of the repo so live queries resolve.
71
+ # Refresh the engine index (incremental; indexes from scratch when absent). This is
72
+ # the valuable side-effect live queries resolve against a current index. A failed
73
+ # refresh writes nothing: a fresh `generated_at` over a frozen index would lie.
73
74
  PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
74
- [[ -n "$PROJECT" ]] || { echo "could not index repo — nothing written" >&2; exit 2; }
75
-
76
- # ...then ALWAYS re-index. memory_ensure_index calls index_repository only when the
77
- # project is ABSENT — correct for the graph-*.sh query wrappers, which must stay
78
- # cheap — so on an already-indexed repo this tool used to write a gate marker with a
79
- # fresh `generated_at` over a frozen index: a deleted symbol stayed resolvable, a new
80
- # one never appeared, and nothing in the output said so. Refreshing is this tool's
81
- # entire job. The engine indexes incrementally, so the repeat call is cheap.
82
- REFRESHED="$(memory_index_bounded "$REPO_ABS" 2>/dev/null | jq -r '.project // empty' 2>/dev/null || true)"
83
- if [[ -z "$REFRESHED" ]]; then
84
- echo "index refresh failed — nothing written" >&2
85
- exit 2
86
- fi
87
- PROJECT="$REFRESHED"
75
+ [[ -n "$PROJECT" ]] || { echo "index refresh failed — nothing written" >&2; exit 2; }
88
76
 
89
77
  mkdir -p "$OUT"
90
78
 
@@ -120,32 +108,29 @@ VER="$("$MEMORY_BIN" --version 2>/dev/null | awk '{print $NF}' || echo unknown)"
120
108
 
121
109
  # Incremental-refresh provenance (graph-tooling-v2 Phase 5): the engine indexes
122
110
  # incrementally (content-based, git-aware), so re-indexing only touches changed
123
- # files. detect_changes reports that working-tree delta — recorded as provenance
124
- # and echoed so a refresh shows what moved. Best-effort: never aborts the write.
111
+ # files. detect_changes reports that working-tree delta — echoed so a refresh
112
+ # shows what moved, but kept out of the committed marker (it differs per machine
113
+ # and per run, like the path-derived project name and a timestamp would).
114
+ # Best-effort: never aborts the write.
125
115
  CHANGES_JSON="$(memory_cli detect_changes "$(jq -n --arg p "$PROJECT" '{project:$p}')" 2>/dev/null || echo '{}')"
126
116
  echo "$CHANGES_JSON" | jq -e . >/dev/null 2>&1 || CHANGES_JSON='{}'
127
117
  CHANGED_FILES="$(echo "$CHANGES_JSON" | jq -r '.changed_count // (.changed_files | length?) // 0' 2>/dev/null || echo 0)"
128
118
  IMPACTED="$(echo "$CHANGES_JSON" | jq -r '(.impacted_symbols | length?) // 0' 2>/dev/null || echo 0)"
129
119
 
130
- # YAML double-quoted scalars: escape backslashes then quotes so an unusual
131
- # project name or engine version string can never corrupt the marker.
132
- PROJECT_Y="${PROJECT//\\/\\\\}"; PROJECT_Y="${PROJECT_Y//\"/\\\"}"
120
+ # YAML double-quoted scalar: escape backslashes then quotes so an unusual
121
+ # engine version string can never corrupt the marker.
133
122
  VER_Y="${VER//\\/\\\\}"; VER_Y="${VER_Y//\"/\\\"}"
134
123
 
135
124
  cat > "$OUT/schema.yaml" <<EOF
136
125
  # Draft graph gate marker — written by scripts/tools/graph-snapshot.sh
137
126
  # Draft is engine-only: this file carries NO graph data. Its presence signals that
138
127
  # the local codebase-memory-mcp engine is wired for this repo. Query the engine
139
- # live via the graph-*.sh wrappers (or \`codebase-memory-mcp cli <tool>\`).
128
+ # live via the graph-*.sh wrappers (graph-query.sh --tool covers the rest).
140
129
  # Counts below are point-of-index provenance; the live engine is authoritative.
141
130
  engine: codebase-memory-mcp
142
131
  engine_version: "$VER_Y"
143
- project: "$PROJECT_Y"
144
- generated_at: "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
145
132
  indexed_nodes: $NODES
146
133
  indexed_edges: $EDGES
147
- changed_files: $CHANGED_FILES
148
- impacted_symbols: $IMPACTED
149
134
  access: engine-live
150
135
  EOF
151
136
 
@@ -4,7 +4,7 @@
4
4
  # graph-tooling-v2 Phase 6. Wraps the engine's ingest_traces to close the
5
5
  # static/dynamic gap — dynamic dispatch the static graph misses (e.g. closures,
6
6
  # reflection, virtual calls). This is a WRITE path and is gated behind
7
- # --experimental. NOTE: in engine v0.8.x ingest_traces is accepted but runtime
7
+ # --experimental. NOTE: in engine v0.9.0 ingest_traces is accepted but runtime
8
8
  # edge creation is "not yet implemented" — the engine returns its status verbatim.
9
9
  #
10
10
  # Usage:
@@ -39,7 +39,7 @@ Flags:
39
39
  --experimental Required acknowledgement — this is a write/experimental path.
40
40
  --help Show this help.
41
41
 
42
- NOTE: engine v0.8.x accepts traces but runtime edge creation is not yet
42
+ NOTE: engine v0.9.0 accepts traces but runtime edge creation is not yet
43
43
  implemented; the engine's status is returned verbatim.
44
44
 
45
45
  Output: raw engine JSON; {"source":"unavailable"} (exit 2) when unavailable.
@@ -79,7 +79,16 @@ echo "$ARCH_JSON" | jq -e . >/dev/null 2>&1 || unavailable
79
79
  # and still reported source:"memory-graph", so a fan-in-only ranking was
80
80
  # indistinguishable from a fully measured one. Record the outcome instead, and
81
81
  # omit the fields rather than emitting measurements that were never taken.
82
- if PROPS_JSON="$(gq_run "$PROJECT" "$(gq_q_node_props)")"; then
82
+ # Ask for the hotspots by name: a capped scan of every node missed hotspots past
83
+ # the window on large repos and scored them 0 under enrichment "ok".
84
+ NAMES=""
85
+ while IFS= read -r q; do
86
+ NAMES+="${NAMES:+,}'$(gq_escape "$q")'"
87
+ done < <(jq -r '(.hotspots // [])[].qualified_name // empty' <<< "$ARCH_JSON")
88
+ if [[ -z "$NAMES" ]]; then
89
+ PROPS_JSON='{"rows":[]}'
90
+ ENRICHMENT="ok"
91
+ elif PROPS_JSON="$(gq_run "$PROJECT" "$(gq_q_node_props "$NAMES")")"; then
83
92
  ENRICHMENT="ok"
84
93
  else
85
94
  PROPS_JSON='{"rows":[]}'
@@ -137,12 +137,24 @@ fi
137
137
 
138
138
  STATUS="ok"
139
139
 
140
+ # The wrappers are verified against the pinned DEFAULT_VERSION. Another version
141
+ # (e.g. a global install on PATH, which outranks the managed one) stays usable,
142
+ # but may speak a different dialect or CLI — say so rather than run it silently.
143
+ VERSION="$("$MEMORY_BIN" --version 2>/dev/null | awk '{print $NF}')"
144
+ PINNED="$(sed -n 's/^DEFAULT_VERSION="v\{0,1\}\([^"]*\)".*/\1/p' "$SELF_REPO/scripts/fetch-memory-engine.sh" 2>/dev/null | head -1)"
145
+ if [[ -n "$PINNED" && "$VERSION" != "$PINNED" ]]; then
146
+ echo "WARNING: engine version $VERSION differs from the pinned $PINNED that Draft's graph tools are verified against." >&2
147
+ echo " Install the pinned engine: scripts/fetch-memory-engine.sh --force" >&2
148
+ fi
149
+
140
150
  if [[ $EMIT_JSON -eq 1 ]]; then
141
- printf '{"status":"%s","engine_bin":"%s","source":"%s","arch":"%s"}\n' \
142
- "$(json_escape "$STATUS")" "$(json_escape "$MEMORY_BIN")" "$(json_escape "$SOURCE")" "$(json_escape "$ARCH")"
151
+ printf '{"status":"%s","engine_bin":"%s","source":"%s","arch":"%s","version":"%s","pinned_version":"%s"}\n' \
152
+ "$(json_escape "$STATUS")" "$(json_escape "$MEMORY_BIN")" "$(json_escape "$SOURCE")" "$(json_escape "$ARCH")" \
153
+ "$(json_escape "$VERSION")" "$(json_escape "$PINNED")"
143
154
  else
144
155
  echo "Draft graph engine: $MEMORY_BIN"
145
156
  echo " source: $SOURCE (arch=$ARCH)"
157
+ echo " version: $VERSION (pinned: ${PINNED:-unknown})"
146
158
  echo " status: $STATUS"
147
159
  fi
148
160
 
@@ -671,7 +671,7 @@ After a phase passes review, refresh `metadata.json.impact` so future tracks can
671
671
  "$DRAFT_TOOLS/graph-impact.sh" --repo . --file <path>
672
672
  ```
673
673
 
674
- Aggregate across all files: `downstream_files` = total unique downstream files (deduped), `downstream_modules` = union of `affected_modules`, `max_depth` = max across queries, `by_category` = sum of each query's `by_category`. If the graph is absent, leave these fields as zeros / empty arrays — the snapshot still records the directly-touched files.
674
+ Aggregate across all files: `downstream_files` = count of the union of each query's `downstream_files` array, `downstream_modules` = union of `affected_modules`, `max_depth` = max across queries, `by_category` = sum of each query's `by_category`. If the graph is absent, leave these fields as zeros / empty arrays — the snapshot still records the directly-touched files.
675
675
 
676
676
  3. **Write metadata.json** with the populated `impact` block and `computed_at` set to the current timestamp.
677
677