@drafthq/draft 3.5.3 → 3.7.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.
Files changed (143) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +1 -1
  4. package/README.md +28 -49
  5. package/bin/README.md +47 -1
  6. package/cli/src/hosts/cursor.js +3 -0
  7. package/cli/src/installer.js +8 -4
  8. package/cli/src/lib/cursor-registry.js +10 -0
  9. package/cli/src/lib/fsx.js +3 -0
  10. package/cli/src/lib/marker.js +8 -3
  11. package/core/agents/architect.md +21 -9
  12. package/core/agents/debugger.md +5 -0
  13. package/core/agents/ops.md +7 -2
  14. package/core/agents/planner.md +7 -0
  15. package/core/agents/rca.md +14 -3
  16. package/core/agents/reviewer.md +1 -0
  17. package/core/agents/writer.md +6 -0
  18. package/core/knowledge-base.md +22 -3
  19. package/core/methodology.md +30 -5
  20. package/core/shared/condensation.md +28 -18
  21. package/core/shared/context-verify.md +3 -1
  22. package/core/shared/cross-skill-dispatch.md +7 -3
  23. package/core/shared/draft-context-loading.md +3 -0
  24. package/core/shared/git-report-metadata.md +2 -1
  25. package/core/shared/graph-query.md +24 -26
  26. package/core/shared/graph-usage-report.md +1 -1
  27. package/core/shared/jira-sync.md +10 -6
  28. package/core/shared/okf-retrieval.md +1 -1
  29. package/core/shared/parallel-analysis.md +14 -7
  30. package/core/shared/pattern-learning.md +2 -0
  31. package/core/shared/template-contract.md +1 -1
  32. package/core/shared/tool-resolver.md +20 -15
  33. package/core/shared/vcs-commands.md +5 -2
  34. package/core/templates/ai-context.md +3 -2
  35. package/core/templates/ai-profile.md +7 -0
  36. package/core/templates/architecture.md +6 -2
  37. package/core/templates/discovery.md +1 -1
  38. package/core/templates/guardrails.md +9 -0
  39. package/core/templates/hld.md +5 -4
  40. package/core/templates/intake-questions.md +45 -0
  41. package/core/templates/jira.md +11 -2
  42. package/core/templates/lld.md +8 -5
  43. package/core/templates/okf/concept.md +0 -1
  44. package/core/templates/plan.md +1 -1
  45. package/core/templates/product.md +11 -2
  46. package/core/templates/rca.md +6 -0
  47. package/core/templates/service-index.md +1 -0
  48. package/core/templates/spec.md +15 -0
  49. package/core/templates/tech-stack.md +12 -0
  50. package/core/templates/workflow.md +20 -0
  51. package/integrations/agents/AGENTS.md +1449 -817
  52. package/integrations/copilot/.github/copilot-instructions.md +1449 -817
  53. package/package.json +1 -1
  54. package/scripts/fetch-memory-engine.sh +25 -7
  55. package/scripts/lib.sh +90 -6
  56. package/scripts/tools/_graph_queries.sh +22 -8
  57. package/scripts/tools/_lib.sh +81 -11
  58. package/scripts/tools/adr-index.sh +1 -1
  59. package/scripts/tools/check-repo-size.sh +125 -0
  60. package/scripts/tools/check-skill-line-caps.sh +3 -4
  61. package/scripts/tools/check-template-noop.sh +0 -1
  62. package/scripts/tools/check-track-hygiene.sh +41 -21
  63. package/scripts/tools/classify-files.sh +3 -3
  64. package/scripts/tools/cycle-detect.sh +18 -17
  65. package/scripts/tools/detect-test-framework.sh +1 -1
  66. package/scripts/tools/emit-skill-metrics.sh +7 -2
  67. package/scripts/tools/fix-whitespace.sh +3 -0
  68. package/scripts/tools/freshness-check.sh +2 -2
  69. package/scripts/tools/git-metadata.sh +12 -6
  70. package/scripts/tools/graph-arch.sh +5 -11
  71. package/scripts/tools/graph-callers.sh +10 -15
  72. package/scripts/tools/graph-deps.sh +3 -10
  73. package/scripts/tools/graph-errors.sh +4 -11
  74. package/scripts/tools/graph-hierarchy.sh +4 -11
  75. package/scripts/tools/graph-impact.sh +21 -19
  76. package/scripts/tools/graph-init.sh +13 -9
  77. package/scripts/tools/graph-preflight.sh +4 -1
  78. package/scripts/tools/graph-query.sh +74 -18
  79. package/scripts/tools/graph-risk.sh +3 -10
  80. package/scripts/tools/graph-search.sh +4 -11
  81. package/scripts/tools/graph-snapshot.sh +38 -9
  82. package/scripts/tools/graph-snippet.sh +3 -10
  83. package/scripts/tools/graph-tests.sh +3 -10
  84. package/scripts/tools/graph-traces.sh +3 -10
  85. package/scripts/tools/hotspot-rank.sh +6 -12
  86. package/scripts/tools/install-smoke-test.sh +223 -0
  87. package/scripts/tools/mermaid-from-graph.sh +5 -11
  88. package/scripts/tools/migrate-track-frontmatter.sh +11 -10
  89. package/scripts/tools/okf-coverage-check.sh +8 -7
  90. package/scripts/tools/okf-emit-catalog.sh +256 -0
  91. package/scripts/tools/okf-fix-links.sh +251 -0
  92. package/scripts/tools/okf-plan-concepts.sh +242 -14
  93. package/scripts/tools/okf-render-views.sh +63 -21
  94. package/scripts/tools/okf-validate-all.sh +20 -9
  95. package/scripts/tools/okf-validate-quality.sh +15 -10
  96. package/scripts/tools/okf-validate.sh +20 -6
  97. package/scripts/tools/parse-git-log.sh +23 -5
  98. package/scripts/tools/parse-reports.sh +1 -1
  99. package/scripts/tools/render-track.sh +1 -1
  100. package/scripts/tools/resolve-tools.sh +24 -8
  101. package/scripts/tools/run-coverage.sh +1 -1
  102. package/scripts/tools/scan-markers.sh +4 -4
  103. package/scripts/tools/validate-frontmatter.sh +2 -2
  104. package/scripts/tools/verify-citations.sh +3 -4
  105. package/scripts/tools/verify-doc-anchors.sh +14 -34
  106. package/scripts/tools/verify-graph-binary.sh +15 -5
  107. package/skills/GRAPH.md +21 -10
  108. package/skills/adr/SKILL.md +25 -7
  109. package/skills/assist-review/SKILL.md +2 -1
  110. package/skills/bughunt/SKILL.md +55 -12
  111. package/skills/bughunt/references/regression-tests.md +14 -4
  112. package/skills/change/SKILL.md +19 -9
  113. package/skills/coverage/SKILL.md +28 -12
  114. package/skills/debug/SKILL.md +13 -3
  115. package/skills/decompose/SKILL.md +42 -12
  116. package/skills/deep-review/SKILL.md +19 -6
  117. package/skills/deploy-checklist/SKILL.md +10 -3
  118. package/skills/documentation/SKILL.md +12 -1
  119. package/skills/draft/SKILL.md +40 -29
  120. package/skills/draft/context-files.md +8 -1
  121. package/skills/draft/quality-guide.md +10 -2
  122. package/skills/graph/SKILL.md +2 -2
  123. package/skills/impact/SKILL.md +4 -2
  124. package/skills/implement/SKILL.md +62 -20
  125. package/skills/incident-response/SKILL.md +5 -2
  126. package/skills/init/SKILL.md +112 -30
  127. package/skills/init/references/architecture-spec.md +32 -8
  128. package/skills/init/references/okf-emitter.md +42 -78
  129. package/skills/jira/SKILL.md +33 -13
  130. package/skills/jira/references/review.md +61 -19
  131. package/skills/learn/SKILL.md +30 -7
  132. package/skills/new-track/SKILL.md +73 -18
  133. package/skills/quick-review/SKILL.md +8 -3
  134. package/skills/revert/SKILL.md +11 -5
  135. package/skills/review/SKILL.md +126 -27
  136. package/skills/review/references/zero-setup-mode.md +117 -0
  137. package/skills/standup/SKILL.md +10 -2
  138. package/skills/status/SKILL.md +7 -7
  139. package/skills/tech-debt/SKILL.md +9 -3
  140. package/skills/testing-strategy/SKILL.md +10 -2
  141. package/skills/tour/SKILL.md +2 -1
  142. package/skills/upload/SKILL.md +4 -3
  143. package/core/templates/track-architecture.md +0 -311
@@ -74,6 +74,29 @@ record() {
74
74
  violation_count=$((violation_count + 1))
75
75
  }
76
76
 
77
+ # Numeric reader for metadata.json:hygiene_budget.* (read_json_str is string-only).
78
+ read_json_num() {
79
+ local file="$1" key="$2"
80
+ [[ -f "$file" ]] || return 0
81
+ grep -oE "\"$key\"[[:space:]]*:[[:space:]]*-?[0-9]+" "$file" 2>/dev/null \
82
+ | head -1 | grep -oE '\-?[0-9]+$' || true
83
+ }
84
+
85
+ # TBD budget for every top-level doc of one track; cap < 0 means unlimited.
86
+ check_tbd_budget() {
87
+ local track_dir="$1" rel_track="$2" cap="$3" kind="$4" detail_suffix="$5"
88
+ (( cap >= 0 )) || return 0
89
+ while IFS= read -r f; do
90
+ local rel_file="${f#"$track_dir/"}"
91
+ local count
92
+ count="$(grep -oE "$TBD_RE" "$f" 2>/dev/null | wc -l | tr -d ' ' || true)"
93
+ if (( count > cap )); then
94
+ record "$rel_track" "$kind" "$rel_file" "0" \
95
+ "$count TBD sentinel(s) $detail_suffix"
96
+ fi
97
+ done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
98
+ }
99
+
77
100
  scan_one_track() {
78
101
  local track_dir="$1"
79
102
  local rel_track="${track_dir#"$REPO_ROOT/"}"
@@ -138,31 +161,28 @@ scan_one_track() {
138
161
  done < "$f"
139
162
  done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
140
163
 
141
- # 4. TBD budget per-doc.
164
+ # 4. TBD budget per-doc. Caps come from metadata.json:hygiene_budget when
165
+ # present (draft_tbd_cap / ready_for_review_tbd_cap; -1 = unlimited),
166
+ # falling back to the historical defaults.
142
167
  case "$meta_status" in
143
- draft|archived) ;;
168
+ archived) ;;
169
+ draft)
170
+ local draft_cap
171
+ draft_cap="$(read_json_num "$meta" "draft_tbd_cap")"
172
+ [[ "$draft_cap" =~ ^-?[0-9]+$ ]] || draft_cap=-1
173
+ check_tbd_budget "$track_dir" "$rel_track" "$draft_cap" "tbd-over-cap" \
174
+ "(cap $draft_cap at status=$meta_status)"
175
+ ;;
144
176
  ready-for-review|in_progress)
145
- local tbd_cap=3
146
- while IFS= read -r f; do
147
- local rel_file="${f#"$track_dir/"}"
148
- local count
149
- count="$(grep -oE "$TBD_RE" "$f" 2>/dev/null | wc -l | tr -d ' ')"
150
- if (( count > tbd_cap )); then
151
- record "$rel_track" "tbd-over-cap" "$rel_file" "0" \
152
- "$count TBD sentinel(s) (cap $tbd_cap at status=$meta_status)"
153
- fi
154
- done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
177
+ local tbd_cap
178
+ tbd_cap="$(read_json_num "$meta" "ready_for_review_tbd_cap")"
179
+ [[ "$tbd_cap" =~ ^-?[0-9]+$ ]] || tbd_cap=3
180
+ check_tbd_budget "$track_dir" "$rel_track" "$tbd_cap" "tbd-over-cap" \
181
+ "(cap $tbd_cap at status=$meta_status)"
155
182
  ;;
156
183
  completed)
157
- while IFS= read -r f; do
158
- local rel_file="${f#"$track_dir/"}"
159
- local count
160
- count="$(grep -oE "$TBD_RE" "$f" 2>/dev/null | wc -l | tr -d ' ')"
161
- if (( count > 0 )); then
162
- record "$rel_track" "tbd-in-completed" "$rel_file" "0" \
163
- "$count TBD sentinel(s) at status=completed"
164
- fi
165
- done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
184
+ check_tbd_budget "$track_dir" "$rel_track" 0 "tbd-in-completed" \
185
+ "at status=completed"
166
186
  ;;
167
187
  esac
168
188
 
@@ -34,14 +34,14 @@ Output (per file): {path, lang, category, is_test, is_generated, loc, module}
34
34
 
35
35
  Language is inferred from extension and shebang. A file is classified as a
36
36
  test when its path or basename matches common test patterns. It is flagged
37
- generated when the first 5 lines contain "Code generated" or "DO NOT EDIT".
38
- Module is the first path segment under root.
37
+ generated when the first 5 lines contain "Code generated", "DO NOT EDIT",
38
+ "@generated", or "autogenerated". Module is the first path segment under root.
39
39
  EOF
40
40
  }
41
41
 
42
42
  while [[ $# -gt 0 ]]; do
43
43
  case "$1" in
44
- --root) ROOT="$2"; shift 2;;
44
+ --root) ROOT="${2:?--root requires a value}"; shift 2;;
45
45
  --json) FORMAT="json"; shift;;
46
46
  --jsonl) FORMAT="jsonl"; shift;;
47
47
  --help|-h) usage; exit 0;;
@@ -40,7 +40,7 @@ EOF
40
40
 
41
41
  while [[ $# -gt 0 ]]; do
42
42
  case "$1" in
43
- --repo) REPO="$2"; shift 2;;
43
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
44
44
  --help|-h) usage; exit 0;;
45
45
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
46
46
  esac
@@ -51,32 +51,33 @@ if [[ ! -d "$REPO" ]]; then
51
51
  exit 1
52
52
  fi
53
53
 
54
- REPO_ABS="$(cd "$REPO" && pwd)"
55
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
56
-
57
54
  unavailable() { echo '{"cycles":[],"source":"unavailable"}'; exit 2; }
58
55
 
59
- find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
60
- command -v jq >/dev/null 2>&1 || unavailable
61
-
62
- PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
63
- [[ -n "$PROJECT" ]] || unavailable
56
+ graph_bootstrap "$REPO" || unavailable
64
57
 
65
58
  # 2- and 3-node CALLS cycles. Cypher lives in _graph_queries.sh (label-agnostic;
66
59
  # the Phase 0 fix — code units are mostly :Method, and CALLS only connects
67
60
  # 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
-
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='{}'
61
+ #
62
+ # A failed query is not an empty result. Substituting `{}` and carrying on
63
+ # reported `source:"memory-graph"` with zero cycles — a clean bill of health
64
+ # derived from a query that never ran, which is exactly the true-negative
65
+ # confusion Guardrail 4 exists to prevent, and the one failure mode a caller
66
+ # cannot detect. Both queries must land for the sample to mean anything, so
67
+ # either failure routes to `unavailable`. gq_run only ever echoes validated
68
+ # JSON, so nothing downstream needs a second shape guard.
69
+ R2="$(gq_run "$PROJECT" "$(gq_q_cycles2)")" || unavailable
70
+ R3="$(gq_run "$PROJECT" "$(gq_q_cycles3)")" || unavailable
74
71
 
72
+ # Self-loops and duplicate orderings are filtered here rather than in Cypher:
73
+ # the engine rejects `a.x < b.x`, which is what the query used to rely on.
74
+ # A 2-cycle comes back twice (A,B and B,A), hence the doubled LIMIT upstream.
75
75
  jq -n --argjson r2 "$R2" --argjson r3 "$R3" '
76
- ( ((($r2.rows) // []) | length) >= 100
76
+ ( ((($r2.rows) // []) | length) >= 200
77
77
  or ((($r3.rows) // []) | length) >= 100 ) as $trunc
78
+ | ( ($r2.rows // []) | map(select(.[0] != .[1])) | unique_by(sort) ) as $two
78
79
  | {
79
- cycles: (((($r2.rows) // []) + (($r3.rows) // []))),
80
+ cycles: ($two + ($r3.rows // [])),
80
81
  truncated: $trunc,
81
82
  source: "memory-graph"
82
83
  }'
@@ -39,7 +39,7 @@ EOF
39
39
 
40
40
  while [[ $# -gt 0 ]]; do
41
41
  case "$1" in
42
- --root) ROOT="$2"; shift 2;;
42
+ --root) ROOT="${2:?--root requires a value}"; shift 2;;
43
43
  --help|-h) usage; exit 0;;
44
44
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
45
45
  esac
@@ -37,8 +37,13 @@ LOCK_FILE="${METRICS_DIR}/metrics.lock"
37
37
 
38
38
  payload="${1:-}"
39
39
 
40
- # Validate that a payload was provided
41
- if [[ -z "${payload}" ]]; then
40
+ # Trim trailing whitespace/newlines so the closing-brace strip below matches
41
+ # even when the caller passes a payload with a stray trailing newline.
42
+ payload="${payload%"${payload##*[![:space:]]}"}"
43
+
44
+ # Validate that a payload was provided and looks like a JSON object — a
45
+ # payload not ending in '}' would corrupt the NDJSON file, so drop it.
46
+ if [[ -z "${payload}" || "${payload}" != *\} ]]; then
42
47
  exit 0
43
48
  fi
44
49
 
@@ -83,6 +83,9 @@ fix_file() {
83
83
  return 1 # already clean — no change on disk
84
84
  fi
85
85
 
86
+ # mktemp creates 0600 and `mv` swaps the inode — carry the destination's mode
87
+ # across, or an atomic rewrite silently strips the file's permissions.
88
+ apply_dest_mode "$_tmp" "$file"
86
89
  mv -f "$_tmp" "$file"
87
90
  return 0
88
91
  }
@@ -46,8 +46,8 @@ EOF
46
46
 
47
47
  while [[ $# -gt 0 ]]; do
48
48
  case "$1" in
49
- --state) STATE_FILE="$2"; shift 2;;
50
- --root) ROOT="$2"; shift 2;;
49
+ --state) STATE_FILE="${2:?--state requires a value}"; shift 2;;
50
+ --root) ROOT="${2:?--root requires a value}"; shift 2;;
51
51
  --help|-h) usage; exit 0;;
52
52
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
53
53
  esac
@@ -66,12 +66,12 @@ while [[ $# -gt 0 ]]; do
66
66
  --yaml) FORMAT="yaml"; shift;;
67
67
  --json) FORMAT="json"; shift;;
68
68
  --project-metadata) WRITE_PROJECT_METADATA=1; shift;;
69
- --project) PROJECT="$2"; shift 2;;
70
- --module) MODULE="$2"; shift 2;;
71
- --track-id) TRACK_ID="$2"; shift 2;;
72
- --generated-by) GENERATED_BY="$2"; shift 2;;
73
- --base) BASE_BRANCH="$2"; shift 2;;
74
- --output-dir) OUTPUT_DIR="$2"; shift 2;;
69
+ --project) PROJECT="${2:?--project requires a value}"; shift 2;;
70
+ --module) MODULE="${2:?--module requires a value}"; shift 2;;
71
+ --track-id) TRACK_ID="${2:?--track-id requires a value}"; shift 2;;
72
+ --generated-by) GENERATED_BY="${2:?--generated-by requires a value}"; shift 2;;
73
+ --base) BASE_BRANCH="${2:?--base requires a value}"; shift 2;;
74
+ --output-dir) OUTPUT_DIR="${2:?--output-dir requires a value}"; shift 2;;
75
75
  --help|-h) usage; exit 0;;
76
76
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
77
77
  esac
@@ -127,6 +127,8 @@ if (( WRITE_PROJECT_METADATA )); then
127
127
  fi
128
128
  CMD="${GENERATED_BY:-draft:init}"
129
129
  _tmp="$(mktemp "${META_FILE}.XXXXXX")"
130
+ # errexit on a failed write would otherwise leave metadata.json.XXXXXX behind.
131
+ trap 'rm -f "$_tmp"' EXIT
130
132
  cat > "$_tmp" <<EOF
131
133
  {
132
134
  "\$schema": "Draft Project Metadata Schema",
@@ -150,7 +152,11 @@ if (( WRITE_PROJECT_METADATA )); then
150
152
  "synced_to_commit": "$FULL_SHA"
151
153
  }
152
154
  EOF
155
+ # mktemp creates 0600 and `mv` swaps the inode — carry the destination's mode
156
+ # across, or an atomic rewrite silently strips the file's permissions.
157
+ apply_dest_mode "$_tmp" "$META_FILE"
153
158
  mv -f "$_tmp" "$META_FILE"
159
+ trap - EXIT
154
160
  echo "Written: $META_FILE (synced_to_commit=$FULL_SHA)"
155
161
  exit 0
156
162
  fi
@@ -42,7 +42,7 @@ EOF
42
42
 
43
43
  while [[ $# -gt 0 ]]; do
44
44
  case "$1" in
45
- --repo) REPO="$2"; shift 2;;
45
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
46
46
  --help|-h) usage; exit 0;;
47
47
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
48
48
  esac
@@ -53,20 +53,14 @@ if [[ ! -d "$REPO" ]]; then
53
53
  exit 1
54
54
  fi
55
55
 
56
- REPO_ABS="$(cd "$REPO" && pwd)"
57
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
58
-
59
56
  unavailable() { echo '{"source":"unavailable"}'; exit 2; }
60
57
 
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
58
+ graph_bootstrap "$REPO" || unavailable
66
59
 
67
- ARCH_JSON="$(memory_cli get_architecture "{\"project\":\"$PROJECT\",\"aspects\":[\"all\"]}" || true)"
60
+ ARCH_JSON="$(memory_cli get_architecture "$(jq -n --arg p "$PROJECT" '{project:$p, aspects:["all"]}')" || true)"
68
61
  [[ -n "$ARCH_JSON" ]] || unavailable
69
62
 
70
63
  # Validate it parses and looks like an architecture object before emitting.
64
+ # Tag with source like every other graph-*.sh wrapper (universal contract).
71
65
  echo "$ARCH_JSON" | jq -e '.total_nodes != null' >/dev/null 2>&1 || unavailable
72
- echo "$ARCH_JSON" | jq '.'
66
+ echo "$ARCH_JSON" | jq '. + {source:"memory-graph"}'
@@ -9,7 +9,10 @@
9
9
  # scripts/tools/graph-callers.sh --repo DIR --symbol NAME
10
10
  # [--transitive[=N]] [--prod-only] [--qualified]
11
11
  #
12
- # Output: JSON {symbol, callers:[{name,file[,hop]}], status, source}.
12
+ # Output: JSON {symbol, callers:[{name,file,qualified[,hop]}], status, source}.
13
+ # `file` is always a path (empty when the engine does not carry one — the
14
+ # trace_path expander behind --transitive returns no file_path); the symbol's
15
+ # qualified name is in `qualified`, never smuggled into `file`.
13
16
  # source = "memory-graph" | "unavailable"
14
17
  # status = "ok" | "no-edges" | "no-match" | "unavailable"
15
18
  # (fail-loud: distinguishes node-not-found from node-has-no-callers
@@ -54,8 +57,8 @@ EOF
54
57
 
55
58
  while [[ $# -gt 0 ]]; do
56
59
  case "$1" in
57
- --repo) REPO="$2"; shift 2;;
58
- --symbol) SYMBOL="$2"; shift 2;;
60
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
61
+ --symbol) SYMBOL="${2:?--symbol requires a value}"; shift 2;;
59
62
  --transitive) TRANSITIVE=1; shift;;
60
63
  --transitive=*) TRANSITIVE=1; DEPTH="${1#*=}"; shift;;
61
64
  --prod-only) PROD_ONLY=1; shift;;
@@ -69,20 +72,13 @@ done
69
72
  [[ -n "$SYMBOL" ]] || { echo "ERROR: --symbol is required" >&2; usage >&2; exit 1; }
70
73
  [[ "$DEPTH" =~ ^[0-9]+$ ]] || { echo "ERROR: --transitive depth must be a non-negative integer" >&2; exit 1; }
71
74
 
72
- REPO_ABS="$(cd "$REPO" && pwd)"
73
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
74
-
75
75
  unavailable() {
76
76
  jq -n --arg s "$SYMBOL" '{symbol:$s, callers:[], status:"unavailable", source:"unavailable"}' 2>/dev/null \
77
77
  || echo '{"callers":[],"status":"unavailable","source":"unavailable"}'
78
78
  exit 2
79
79
  }
80
80
 
81
- find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
82
- command -v jq >/dev/null 2>&1 || unavailable
83
-
84
- PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
85
- [[ -n "$PROJECT" ]] || unavailable
81
+ graph_bootstrap "$REPO" || unavailable
86
82
 
87
83
  SYM_ESC="$(gq_escape "$SYMBOL")"
88
84
 
@@ -95,12 +91,11 @@ if [[ "$TRANSITIVE" -eq 1 ]]; then
95
91
  echo "$RES" | jq -e . >/dev/null 2>&1 || unavailable
96
92
  N="$(echo "$RES" | jq -r '(.callers // []) | length' 2>/dev/null || echo 0)"
97
93
  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
94
+ STATUS="$(gq_symbol_status "$PROJECT" "$SYM_ESC" '{"rows":[]}')"
100
95
  fi
101
96
  echo "$RES" | jq --arg s "$SYMBOL" --arg st "$STATUS" '
102
97
  {symbol:$s,
103
- callers: [ (.callers // [])[] | {name:.name, file:(.qualified_name // ""), hop:(.hop // 1)} ],
98
+ callers: [ (.callers // [])[] | {name:.name, file:(.file_path // ""), qualified:(.qualified_name // ""), hop:(.hop // 1)} ],
104
99
  status:$st, source:"memory-graph"}'
105
100
  exit 0
106
101
  fi
@@ -121,5 +116,5 @@ STATUS="$(gq_symbol_status "$PROJECT" "$SYM_ESC" "$RES")"
121
116
 
122
117
  echo "$RES" | jq --arg s "$SYMBOL" --arg st "$STATUS" '
123
118
  {symbol:$s,
124
- callers: [ (.rows // [])[] | {name:.[0], file:.[1]} ],
119
+ callers: [ (.rows // [])[] | {name:.[0], file:(.[1] // ""), qualified:""} ],
125
120
  status:$st, source:"memory-graph"}'
@@ -43,8 +43,8 @@ EOF
43
43
 
44
44
  while [[ $# -gt 0 ]]; do
45
45
  case "$1" in
46
- --repo) REPO="$2"; shift 2;;
47
- --file) FILE="$2"; shift 2;;
46
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
47
+ --file) FILE="${2:?--file requires a value}"; shift 2;;
48
48
  --help|-h) usage; exit 0;;
49
49
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
50
50
  esac
@@ -52,16 +52,9 @@ done
52
52
 
53
53
  [[ -d "$REPO" ]] || { echo "ERROR: --repo '$REPO' is not a directory" >&2; exit 1; }
54
54
 
55
- REPO_ABS="$(cd "$REPO" && pwd)"
56
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
57
-
58
55
  unavailable() { echo '{"imports":[],"total":0,"source":"unavailable"}'; exit 2; }
59
56
 
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
57
+ graph_bootstrap "$REPO" || unavailable
65
58
 
66
59
  RES="$(gq_run "$PROJECT" "$(gq_q_imports)" || true)"
67
60
  [[ -n "$RES" ]] || unavailable
@@ -42,9 +42,9 @@ EOF
42
42
 
43
43
  while [[ $# -gt 0 ]]; do
44
44
  case "$1" in
45
- --repo) REPO="$2"; shift 2;;
46
- --symbol) SYMBOL="$2"; shift 2;;
47
- --type) TYPE="$2"; shift 2;;
45
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
46
+ --symbol) SYMBOL="${2:?--symbol requires a value}"; shift 2;;
47
+ --type) TYPE="${2:?--type requires a value}"; shift 2;;
48
48
  --help|-h) usage; exit 0;;
49
49
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
50
50
  esac
@@ -56,9 +56,6 @@ if [[ -n "$SYMBOL" && -n "$TYPE" ]]; then
56
56
  fi
57
57
  [[ -n "$SYMBOL" || -n "$TYPE" ]] || { echo "ERROR: provide --symbol or --type" >&2; usage >&2; exit 1; }
58
58
 
59
- REPO_ABS="$(cd "$REPO" && pwd)"
60
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
61
-
62
59
  unavailable() {
63
60
  if [[ -n "$TYPE" ]]; then
64
61
  jq -n --arg t "$TYPE" '{type:$t, raisers:[], status:"unavailable", source:"unavailable"}' 2>/dev/null \
@@ -70,11 +67,7 @@ unavailable() {
70
67
  exit 2
71
68
  }
72
69
 
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
70
+ graph_bootstrap "$REPO" || unavailable
78
71
 
79
72
  if [[ -n "$TYPE" ]]; then
80
73
  SYM_ESC="$(gq_escape "$TYPE")"
@@ -43,9 +43,9 @@ EOF
43
43
 
44
44
  while [[ $# -gt 0 ]]; do
45
45
  case "$1" in
46
- --repo) REPO="$2"; shift 2;;
47
- --symbol) SYMBOL="$2"; shift 2;;
48
- --derived) DERIVED="$2"; shift 2;;
46
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
47
+ --symbol) SYMBOL="${2:?--symbol requires a value}"; shift 2;;
48
+ --derived) DERIVED="${2:?--derived requires a value}"; shift 2;;
49
49
  --help|-h) usage; exit 0;;
50
50
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
51
51
  esac
@@ -56,16 +56,9 @@ if [[ -n "$SYMBOL" && -n "$DERIVED" ]]; then
56
56
  echo "ERROR: use either --symbol or --derived, not both" >&2; exit 1
57
57
  fi
58
58
 
59
- REPO_ABS="$(cd "$REPO" && pwd)"
60
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
61
-
62
59
  unavailable() { echo '{"edges":[],"status":"unavailable","source":"unavailable"}'; exit 2; }
63
60
 
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
61
+ graph_bootstrap "$REPO" || unavailable
69
62
 
70
63
  if [[ -n "$SYMBOL" ]]; then
71
64
  SYM_ESC="$(gq_escape "$SYMBOL")"; Q="$(gq_q_inherits_sym "$SYM_ESC")"; PROBE="$SYM_ESC"
@@ -9,7 +9,9 @@
9
9
  # Usage:
10
10
  # scripts/tools/graph-impact.sh --repo DIR (--file PATH | --symbol NAME) [--depth N]
11
11
  #
12
- # Output: JSON {target, kind, impacted:[{name,file,hop}], source}.
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.
13
15
  # source = "memory-graph" | "unavailable"
14
16
  #
15
17
  # Exit codes: 0 OK, 1 invocation error, 2 graph engine unavailable.
@@ -43,10 +45,10 @@ EOF
43
45
 
44
46
  while [[ $# -gt 0 ]]; do
45
47
  case "$1" in
46
- --repo) REPO="$2"; shift 2;;
47
- --file) FILE="$2"; shift 2;;
48
- --symbol) SYMBOL="$2"; shift 2;;
49
- --depth) DEPTH="$2"; shift 2;;
48
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
49
+ --file) FILE="${2:?--file requires a value}"; shift 2;;
50
+ --symbol) SYMBOL="${2:?--symbol requires a value}"; shift 2;;
51
+ --depth) DEPTH="${2:?--depth requires a value}"; shift 2;;
50
52
  --help|-h) usage; exit 0;;
51
53
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
52
54
  esac
@@ -56,9 +58,6 @@ done
56
58
  [[ -n "$FILE" || -n "$SYMBOL" ]] || { echo "ERROR: provide --file or --symbol" >&2; usage >&2; exit 1; }
57
59
  [[ "$DEPTH" =~ ^[0-9]+$ ]] || { echo "ERROR: --depth must be a non-negative integer" >&2; exit 1; }
58
60
 
59
- REPO_ABS="$(cd "$REPO" && pwd)"
60
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
61
-
62
61
  unavailable() {
63
62
  local t="$1" k="$2"
64
63
  jq -n --arg t "$t" --arg k "$k" '{target:$t, kind:$k, impacted:[], source:"unavailable"}' 2>/dev/null \
@@ -68,24 +67,27 @@ unavailable() {
68
67
 
69
68
  if [[ -n "$SYMBOL" ]]; then TARGET="$SYMBOL"; KIND="symbol"; else TARGET="$FILE"; KIND="file"; fi
70
69
 
71
- find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable "$TARGET" "$KIND"
72
- command -v jq >/dev/null 2>&1 || unavailable "$TARGET" "$KIND"
73
-
74
- PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
75
- [[ -n "$PROJECT" ]] || unavailable "$TARGET" "$KIND"
70
+ graph_bootstrap "$REPO" || unavailable "$TARGET" "$KIND"
76
71
 
77
72
  if [[ -n "$SYMBOL" ]]; then
78
73
  # direction:"both" is the reliable form (the "callers" value returns empty in this engine);
79
- # we read the .callers array from it.
80
- RES="$(memory_cli trace_path "{\"project\":\"$PROJECT\",\"function_name\":\"$SYMBOL\",\"depth\":$DEPTH,\"direction\":\"both\"}" || echo '{}')"
81
- echo "${RES:-{\}}" | jq --arg t "$TARGET" '
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 . >/dev/null 2>&1 || unavailable "$TARGET" "$KIND"
81
+ echo "$RES" | jq --arg t "$TARGET" '
82
82
  {target:$t, kind:"symbol",
83
- impacted: [ (.callers // [])[] | {name:.name, file:(.qualified_name // ""), hop:(.hop // 1)} ],
83
+ impacted: [ (.callers // [])[] | {name:.name, file:(.file_path // ""), qualified:(.qualified_name // ""), hop:(.hop // 1)} ],
84
84
  source:"memory-graph"}'
85
85
  else
86
86
  # File impact: detect_changes maps the working-tree diff to impacted symbols.
87
- RES="$(memory_cli detect_changes "{\"project\":\"$PROJECT\"}" || echo '{}')"
88
- echo "${RES:-{\}}" | jq --arg t "$TARGET" '
87
+ PAYLOAD="$(jq -n --arg p "$PROJECT" '{project:$p}')"
88
+ RES="$(memory_cli detect_changes "$PAYLOAD" 2>/dev/null || true)"
89
+ echo "$RES" | jq -e . >/dev/null 2>&1 || unavailable "$TARGET" "$KIND"
90
+ echo "$RES" | jq --arg t "$TARGET" '
89
91
  {target:$t, kind:"file",
90
92
  impacted: [ (.impacted_symbols // [])[]
91
93
  | select((.file // "") | endswith($t) or (. == $t))
@@ -52,7 +52,7 @@ EOF
52
52
 
53
53
  while [[ $# -gt 0 ]]; do
54
54
  case "$1" in
55
- --scope) SCOPE="$2"; shift 2;;
55
+ --scope) SCOPE="${2:?--scope requires a value}"; shift 2;;
56
56
  --module-only) MODULE_ONLY=1; shift;;
57
57
  --no-fetch) NO_FETCH=1; shift;;
58
58
  --json) EMIT_JSON=1; shift;;
@@ -93,7 +93,7 @@ ensure_engine() {
93
93
  engine_unavailable() {
94
94
  if [[ "$EMIT_JSON" -eq 1 ]]; then
95
95
  printf '{"status":"unavailable","root":"%s","scope":"%s","is_root":%s}\n' \
96
- "$ROOT_ABS" "$SCOPE_ABS" "$IS_ROOT"
96
+ "$(json_escape "$ROOT_ABS")" "$(json_escape "$SCOPE_ABS")" "$IS_ROOT"
97
97
  else
98
98
  echo "WARNING: knowledge-graph engine (codebase-memory-mcp) is unavailable — no graph built." >&2
99
99
  echo " The engine is Draft's default capability tier. Install it with:" >&2
@@ -115,7 +115,7 @@ build_snapshot() {
115
115
  # Path from <module>/draft/graph back to <root>/draft/graph (module is under root).
116
116
  root_link_relpath() {
117
117
  local sub="${SCOPE_ABS#"$ROOT_ABS"/}"
118
- local ups=2 seg
118
+ local ups seg
119
119
  IFS='/' read -ra seg <<< "$sub"
120
120
  ups=$(( ${#seg[@]} + 2 ))
121
121
  local i out=""
@@ -135,13 +135,16 @@ write_root_link() {
135
135
  fi
136
136
  root_commit="$(git -C "$ROOT_ABS" rev-parse --verify --quiet HEAD 2>/dev/null || echo none)"
137
137
  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.
138
141
  cat > "$mod_graph/root-link.json" <<EOF
139
142
  {
140
- "root_graph": "$rel",
141
- "root_abs": "$ROOT_ABS/draft/graph",
142
- "root_project": "${root_project:-unknown}",
143
- "root_commit": "$root_commit",
144
- "status": "$status",
143
+ "root_graph": "$(json_escape "$rel")",
144
+ "root_abs": "$(json_escape "$ROOT_ABS/draft/graph")",
145
+ "root_project": "$(json_escape "${root_project:-unknown}")",
146
+ "root_commit": "$(json_escape "$root_commit")",
147
+ "status": "$(json_escape "$status")",
145
148
  "linked_at": "$ts",
146
149
  "linked_by": "graph-init.sh",
147
150
  "note": "Root is the authoritative whole-repo graph. Follow root_graph for cross-module understanding."
@@ -173,7 +176,8 @@ fi
173
176
 
174
177
  if [[ "$EMIT_JSON" -eq 1 ]]; then
175
178
  printf '{"status":"ok","root":"%s","scope":"%s","is_root":%s,"root_built":%s,"module_built":%s,"link_status":"%s"}\n' \
176
- "$ROOT_ABS" "$SCOPE_ABS" "$IS_ROOT" "$ROOT_BUILT" "$MODULE_BUILT" "$LINK_STATUS"
179
+ "$(json_escape "$ROOT_ABS")" "$(json_escape "$SCOPE_ABS")" \
180
+ "$IS_ROOT" "$ROOT_BUILT" "$MODULE_BUILT" "$(json_escape "$LINK_STATUS")"
177
181
  else
178
182
  echo "--- graph-init ---"
179
183
  echo "Root: $ROOT_ABS$([[ $IS_ROOT -eq 1 ]] && echo ' (this scope is the root)')"
@@ -168,9 +168,12 @@ 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
+ # Numeric-only: the value is emitted bare into the --json report, so a
172
+ # non-numeric field (engine output format drift) would produce invalid JSON.
171
173
  LIMIT="$("$ENGINE" config list 2>/dev/null | awk '/auto_index_limit/{print $3}' || true)"
174
+ [[ "$LIMIT" =~ ^[0-9]+$ ]] || LIMIT=""
172
175
  [[ -n "$LIMIT" ]] && info "auto_index_limit: $LIMIT (governs AUTO-index only; explicit index_repository should bypass)" || true
173
- if [[ "$GIT_OK" -eq 1 && -n "${LIMIT:-}" && "$TRACKED" -gt "$LIMIT" ]]; then
176
+ if [[ "$GIT_OK" -eq 1 && "${LIMIT:-}" =~ ^[0-9]+$ && "$TRACKED" -gt "$LIMIT" ]]; then
174
177
  warn "Tracked files ($TRACKED) > auto_index_limit ($LIMIT) — confirm the explicit index isn't truncated near $LIMIT."
175
178
  fi
176
179
  else