@drafthq/draft 3.5.2 → 3.6.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.
Files changed (86) 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/cli/src/hosts/cursor.js +3 -0
  5. package/cli/src/installer.js +7 -3
  6. package/cli/src/lib/fsx.js +3 -0
  7. package/cli/src/lib/marker.js +8 -3
  8. package/core/shared/condensation.md +1 -1
  9. package/core/shared/git-report-metadata.md +1 -1
  10. package/core/shared/graph-query.md +24 -24
  11. package/core/shared/tool-resolver.md +20 -15
  12. package/core/templates/okf/index.md +6 -10
  13. package/core/templates/okf/section-index.md +2 -11
  14. package/core/templates/plan.md +1 -1
  15. package/integrations/agents/AGENTS.md +142 -492
  16. package/integrations/copilot/.github/copilot-instructions.md +142 -492
  17. package/package.json +1 -1
  18. package/scripts/fetch-memory-engine.sh +1 -1
  19. package/scripts/lib.sh +79 -2
  20. package/scripts/tools/_graph_queries.sh +12 -5
  21. package/scripts/tools/_lib.sh +56 -8
  22. package/scripts/tools/adr-index.sh +1 -1
  23. package/scripts/tools/check-skill-line-caps.sh +2 -2
  24. package/scripts/tools/check-track-hygiene.sh +41 -21
  25. package/scripts/tools/classify-files.sh +1 -1
  26. package/scripts/tools/cycle-detect.sh +2 -9
  27. package/scripts/tools/detect-test-framework.sh +1 -1
  28. package/scripts/tools/emit-skill-metrics.sh +7 -2
  29. package/scripts/tools/freshness-check.sh +2 -2
  30. package/scripts/tools/git-metadata.sh +6 -6
  31. package/scripts/tools/graph-arch.sh +4 -10
  32. package/scripts/tools/graph-callers.sh +4 -12
  33. package/scripts/tools/graph-deps.sh +3 -10
  34. package/scripts/tools/graph-errors.sh +4 -11
  35. package/scripts/tools/graph-hierarchy.sh +4 -11
  36. package/scripts/tools/graph-impact.sh +17 -17
  37. package/scripts/tools/graph-init.sh +1 -1
  38. package/scripts/tools/graph-preflight.sh +1 -1
  39. package/scripts/tools/graph-query.sh +5 -12
  40. package/scripts/tools/graph-risk.sh +3 -10
  41. package/scripts/tools/graph-search.sh +4 -11
  42. package/scripts/tools/graph-snapshot.sh +9 -4
  43. package/scripts/tools/graph-snippet.sh +3 -10
  44. package/scripts/tools/graph-tests.sh +3 -10
  45. package/scripts/tools/graph-traces.sh +3 -10
  46. package/scripts/tools/hotspot-rank.sh +4 -11
  47. package/scripts/tools/mermaid-from-graph.sh +3 -10
  48. package/scripts/tools/migrate-track-frontmatter.sh +3 -1
  49. package/scripts/tools/okf-coverage-check.sh +24 -6
  50. package/scripts/tools/okf-emit-catalog.sh +256 -0
  51. package/scripts/tools/okf-fix-links.sh +326 -0
  52. package/scripts/tools/okf-plan-concepts.sh +245 -14
  53. package/scripts/tools/okf-render-views.sh +65 -13
  54. package/scripts/tools/okf-validate-all.sh +20 -9
  55. package/scripts/tools/okf-validate-quality.sh +2 -8
  56. package/scripts/tools/okf-validate.sh +20 -10
  57. package/scripts/tools/parse-git-log.sh +4 -4
  58. package/scripts/tools/parse-reports.sh +1 -1
  59. package/scripts/tools/render-track.sh +1 -1
  60. package/scripts/tools/run-coverage.sh +1 -1
  61. package/scripts/tools/scan-markers.sh +4 -4
  62. package/scripts/tools/validate-frontmatter.sh +2 -2
  63. package/scripts/tools/verify-citations.sh +2 -2
  64. package/scripts/tools/verify-doc-anchors.sh +1 -1
  65. package/scripts/tools/verify-graph-binary.sh +2 -2
  66. package/skills/adr/SKILL.md +1 -1
  67. package/skills/bughunt/SKILL.md +1 -1
  68. package/skills/coverage/SKILL.md +2 -2
  69. package/skills/debug/SKILL.md +1 -1
  70. package/skills/decompose/SKILL.md +3 -3
  71. package/skills/deep-review/SKILL.md +2 -2
  72. package/skills/deploy-checklist/SKILL.md +1 -1
  73. package/skills/graph/SKILL.md +1 -1
  74. package/skills/impact/SKILL.md +1 -1
  75. package/skills/implement/SKILL.md +2 -2
  76. package/skills/init/SKILL.md +10 -7
  77. package/skills/init/references/architecture-spec.md +1 -1
  78. package/skills/init/references/okf-emitter.md +51 -76
  79. package/skills/learn/SKILL.md +1 -1
  80. package/skills/quick-review/SKILL.md +1 -1
  81. package/skills/review/SKILL.md +3 -3
  82. package/skills/standup/SKILL.md +1 -1
  83. package/skills/status/SKILL.md +1 -1
  84. package/skills/tech-debt/SKILL.md +2 -2
  85. package/skills/upload/SKILL.md +1 -1
  86. package/core/templates/track-architecture.md +0 -311
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drafthq/draft",
3
- "version": "3.5.2",
3
+ "version": "3.6.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"
@@ -26,7 +26,7 @@ VERSION="${CMM_VERSION:-$DEFAULT_VERSION}"
26
26
  DEST="$HOME/.cache/draft/bin"
27
27
  FORCE=0
28
28
 
29
- usage() { sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; }
29
+ usage() { sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; }
30
30
 
31
31
  while [[ $# -gt 0 ]]; do
32
32
  case "$1" in
package/scripts/lib.sh CHANGED
@@ -57,6 +57,76 @@ SKILL_ORDER=(
57
57
  assist-review
58
58
  )
59
59
 
60
+ # ─────────────────────────────────────────────────────────
61
+ # Skill metadata: one row per skill — "<name>|<display header>|<copilot trigger>"
62
+ # The name and header never contain "|"; the trigger may (e.g. "[file|pr N]"),
63
+ # so lookups split on the FIRST TWO pipes only. Coverage against SKILL_ORDER is
64
+ # enforced by tests/test-trigger-functions.sh.
65
+ # ─────────────────────────────────────────────────────────
66
+
67
+ SKILL_META=(
68
+ 'draft|Draft Overview|"help" or "draft"'
69
+ 'init|Init Command|"init draft", "build the code graph", or "draft init [refresh] [--graph-only] [--module-only]"'
70
+ 'graph|Graph Command|"build graph", "refresh graph", or "draft graph [path]"'
71
+ 'new-track|New Track Command|"new feature" or "draft new-track <description>"'
72
+ 'decompose|Decompose Command|"break into modules" or "draft decompose"'
73
+ 'implement|Implement Command|"implement" or "draft implement"'
74
+ 'coverage|Coverage Command|"check coverage" or "draft coverage"'
75
+ 'deploy-checklist|Deploy Checklist Command|"deploy checklist" or "draft deploy-checklist [track <id>]"'
76
+ 'bughunt|Bug Hunt Command|"hunt bugs" or "draft bughunt [--track <id>]"'
77
+ 'review|Review Command|"review code" or "draft review [--track <id>] [--full]"'
78
+ 'upload|Upload Command|"upload for review" or "draft upload [track <id>]"'
79
+ 'plan|Plan Router|"plan feature" or "draft plan <intent>" (new-track, decompose, adr, tech-debt, change)'
80
+ 'ops|Ops Router|"ops deploy" or "draft ops <intent>" (deploy-checklist, incident, standup, status, revert)'
81
+ 'docs|Docs Router|"write docs" or "draft docs <intent>" (documentation)'
82
+ 'discover|Discover Router|"discover debug" or "draft discover <intent>" (debug, bughunt, reviews, coverage, learn, index, etc.)'
83
+ 'jira|Jira Router|"jira preview", "jira create", or "jira review <ID>"'
84
+ 'integrations|Integrations Router|"integrations", "integrations jira-preview", or "integrations jira-create"'
85
+ 'quick-review|Quick Review Command|"quick review" or "draft quick-review [file|pr <number>]"'
86
+ 'deep-review|Deep Review Command|"deep review" or "draft deep-review [module]"'
87
+ 'testing-strategy|Testing Strategy Command|"test strategy" or "draft testing-strategy [track <id>|path]"'
88
+ 'learn|Learn Command|"learn patterns" or "draft learn [promote|migrate|path]"'
89
+ 'adr|ADR Command|"document decision" or "draft adr [title]"'
90
+ 'debug|Debug Command|"debug bug" or "draft debug [description|track <id>]"'
91
+ 'standup|Standup Command|"standup" or "draft standup [date|week|save]"'
92
+ 'tech-debt|Tech Debt Command|"tech debt" or "draft tech-debt [path|track <id>]"'
93
+ 'incident-response|Incident Response Command|"incident" or "draft incident-response [new|update|postmortem]"'
94
+ 'documentation|Documentation Command|"write docs" or "draft documentation [readme|runbook|api|onboarding]"'
95
+ 'status|Status Command|"status" or "draft status"'
96
+ 'revert|Revert Command|"revert" or "draft revert"'
97
+ 'change|Change Command|"handle change" or "draft change <description>"'
98
+ 'tour|Tour Command|"tour" or "draft tour"'
99
+ 'impact|Impact Command|"impact" or "draft impact"'
100
+ 'assist-review|Assist Review Command|"assist review" or "draft assist-review"'
101
+ )
102
+
103
+ # get_skill_header <skill> — display header for integration sections.
104
+ get_skill_header() {
105
+ local skill="$1" row rest
106
+ for row in "${SKILL_META[@]}"; do
107
+ if [[ "${row%%|*}" == "$skill" ]]; then
108
+ rest="${row#*|}"
109
+ printf '%s\n' "${rest%%|*}"
110
+ return 0
111
+ fi
112
+ done
113
+ printf '%s\n' "$(echo "${skill:0:1}" | tr '[:lower:]' '[:upper:]')${skill:1} Command"
114
+ }
115
+
116
+ # get_copilot_trigger <skill> — natural-language trigger for the Copilot header.
117
+ get_copilot_trigger() {
118
+ local skill="$1" row rest
119
+ for row in "${SKILL_META[@]}"; do
120
+ if [[ "${row%%|*}" == "$skill" ]]; then
121
+ rest="${row#*|}"
122
+ printf '%s\n' "${rest#*|}"
123
+ return 0
124
+ fi
125
+ done
126
+ printf '"draft %s"\n' "$skill"
127
+ }
128
+
129
+
60
130
  # ─────────────────────────────────────────────────────────
61
131
  # Core reference files (inlined by Claude plugin at runtime)
62
132
  # ─────────────────────────────────────────────────────────
@@ -91,7 +161,6 @@ CORE_FILES=(
91
161
  "templates/ai-context.md"
92
162
  "templates/ai-profile.md"
93
163
  "templates/architecture.md"
94
- "templates/track-architecture.md"
95
164
  "templates/jira.md"
96
165
  "templates/product.md"
97
166
  "templates/tech-stack.md"
@@ -202,6 +271,8 @@ TOOLS=(
202
271
  "okf-validate-quality.sh"
203
272
  "okf-coverage-check.sh"
204
273
  "okf-validate-all.sh"
274
+ "okf-emit-catalog.sh"
275
+ "okf-fix-links.sh"
205
276
  )
206
277
 
207
278
  # ─────────────────────────────────────────────────────────
@@ -264,12 +335,18 @@ extract_body() {
264
335
  }
265
336
 
266
337
  # Validate body format: line 1 blank, line 2 starts with #, line 3 blank.
338
+ # $3 (optional): pre-extracted body — skips a second extract_body parse when
339
+ # the caller already has it.
267
340
  validate_skill_body_format() {
268
341
  local skill="$1"
269
342
  local skill_file="$2"
270
343
 
271
344
  local body_head line1 line2 line3
272
- body_head=$(extract_body "$skill_file" | sed -n '1,3p' || true)
345
+ if (( $# >= 3 )); then
346
+ body_head=$(printf '%s\n' "$3" | sed -n '1,3p')
347
+ else
348
+ body_head=$(extract_body "$skill_file" | sed -n '1,3p' || true)
349
+ fi
273
350
  line1=$(echo "$body_head" | sed -n '1p')
274
351
  line2=$(echo "$body_head" | sed -n '2p')
275
352
  line3=$(echo "$body_head" | sed -n '3p')
@@ -28,9 +28,12 @@ if [[ "$(type -t memory_cli 2>/dev/null)" != function ]]; then
28
28
  source "$_GQ_DIR/_lib.sh"
29
29
  fi
30
30
 
31
- # Escape single quotes for embedding inside a Cypher single-quoted string literal.
31
+ # Escape for embedding inside a Cypher single-quoted string literal.
32
+ # Backslashes must be doubled BEFORE quotes are escaped, otherwise an input
33
+ # ending in `\` yields `\'` — an escaped quote that never closes the literal.
32
34
  gq_escape() {
33
35
  local s="$1"
36
+ s="${s//\\/\\\\}"
34
37
  printf '%s' "${s//\'/\\\'}"
35
38
  }
36
39
 
@@ -82,19 +85,23 @@ gq_rows_len() {
82
85
  printf '%s' "$1" | jq -r '(.rows // []) | length' 2>/dev/null || printf '0'
83
86
  }
84
87
 
85
- # gq_symbol_status <project> <sym_esc> <result_json> -> ok | no-edges | no-match
88
+ # gq_symbol_status <project> <sym_esc> <result_json>
89
+ # -> ok | no-edges | no-match | probe-failed
86
90
  # Fail-loud disambiguation (Guardrail 4): an empty result is only a true negative
87
91
  # ("no-edges") when the node actually exists; otherwise it is "no-match". An
88
92
  # existence probe runs only when the primary result is empty (hot path stays one
89
- # query).
93
+ # query). If the probe itself errors, that is NOT a true negative — report
94
+ # "probe-failed" so callers never mistake an engine failure for absence.
90
95
  gq_symbol_status() {
91
96
  local project="$1" sym="$2" result="$3"
92
97
  if [[ "$(gq_rows_len "$result")" -gt 0 ]]; then
93
98
  printf 'ok'; return
94
99
  fi
95
100
  local ex
96
- ex="$(gq_run "$project" "$(gq_q_exists "$sym")" || true)"
97
- if [[ -n "$ex" && "$(gq_rows_len "$ex")" -gt 0 ]]; then
101
+ if ! ex="$(gq_run "$project" "$(gq_q_exists "$sym")")"; then
102
+ printf 'probe-failed'; return
103
+ fi
104
+ if [[ "$(gq_rows_len "$ex")" -gt 0 ]]; then
98
105
  printf 'no-edges'
99
106
  else
100
107
  printf 'no-match'
@@ -113,6 +113,47 @@ get_yaml_field() {
113
113
  # Sets MEMORY_BIN globally; returns 0 if found, 1 otherwise.
114
114
  # Preference: PATH > Draft-managed install (~/.cache/draft/bin) > vendored bin/<arch> under known roots.
115
115
  # No legacy fallbacks: the Aether `graph`/`graph-clang` binaries are retired.
116
+
117
+ # GitHub-flavored-markdown-ish heading id (shared by TOC, anchors, link fixers).
118
+ # Keeps underscores; strips other punctuation; collapses whitespace to '-'.
119
+ gfm_slug() {
120
+ local s="${1:-}"
121
+ # shellcheck disable=SC2001
122
+ s="$(printf '%s' "$s" | tr '[:upper:]' '[:lower:]')"
123
+ s="$(printf '%s' "$s" | sed -E 's/[^[:alnum:]_[:space:]-]+//g; s/[[:space:]]+/-/g; s/-+/-/g; s/^-+//; s/-+$//')"
124
+ printf '%s' "$s"
125
+ }
126
+
127
+ # Count entries in an x-grounded-paths frontmatter field.
128
+ # Supports both inline (`x-grounded-paths: ["a", "b"]`) and block lists:
129
+ # x-grounded-paths:
130
+ # - "a"
131
+ # - "b"
132
+ grounded_paths_count() {
133
+ local file="$1"
134
+ [[ -f "$file" ]] || { echo 0; return; }
135
+ awk '
136
+ NR==1 && /^---$/ { fm=1; next }
137
+ fm && /^---$/ { exit }
138
+ fm && /^x-grounded-paths:[[:space:]]*\[/ {
139
+ line=$0
140
+ sub(/^x-grounded-paths:[[:space:]]*\[/, "", line)
141
+ sub(/\].*$/, "", line)
142
+ gsub(/[[:space:]]/, "", line)
143
+ if (line=="") { print 0; exit }
144
+ n=split(line, a, ",")
145
+ print n
146
+ exit
147
+ }
148
+ fm && /^x-grounded-paths:[[:space:]]*$/ { collect=1; next }
149
+ fm && collect {
150
+ if ($0 ~ /^[[:space:]]*-[[:space:]]*/) { n++; next }
151
+ if ($0 ~ /^[A-Za-z0-9_-]+:/) { print n+0; exit }
152
+ }
153
+ END { if (collect) print n+0 }
154
+ ' "$file"
155
+ }
156
+
116
157
  find_memory_bin() {
117
158
  local repo_abs="$1"
118
159
  local self_repo="$2"
@@ -157,14 +198,6 @@ find_memory_bin() {
157
198
  local roots=()
158
199
  [[ -n "$repo_abs" && -d "$repo_abs" ]] && roots+=("$repo_abs")
159
200
  [[ -n "$self_repo" && -d "$self_repo" ]] && roots+=("$self_repo")
160
- for bc in \
161
- "$HOME/.cursor/plugins/local/draft/.draft-install-path" \
162
- "$HOME/.claude/plugins/draft/.draft-install-path"; do
163
- if [[ -f "$bc" ]]; then
164
- local pr; pr="$(cat "$bc" 2>/dev/null || true)"
165
- [[ -n "$pr" && -d "$pr" ]] && roots+=("$pr")
166
- fi
167
- done
168
201
 
169
202
  for pr in "${roots[@]}"; do
170
203
  local cand="$pr/bin/$ARCH/$bin_name"
@@ -177,6 +210,21 @@ find_memory_bin() {
177
210
  return 1
178
211
  }
179
212
 
213
+ # graph_bootstrap <repo-dir>
214
+ # Shared engine bootstrap for the graph-*.sh wrappers: resolves the repo path,
215
+ # locates the engine binary, checks jq, and ensures the index. Sets the globals
216
+ # REPO_ABS and PROJECT. Returns 1 on any failure so each wrapper routes it to
217
+ # its own unavailable-JSON shape (the shapes differ per tool).
218
+ graph_bootstrap() {
219
+ local repo="$1" self_repo
220
+ REPO_ABS="$(cd "$repo" 2>/dev/null && pwd)" || return 1
221
+ self_repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
222
+ find_memory_bin "$REPO_ABS" "$self_repo" || return 1
223
+ command -v jq >/dev/null 2>&1 || return 1
224
+ PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
225
+ [[ -n "$PROJECT" ]] || return 1
226
+ }
227
+
180
228
  # Run a codebase-memory-mcp CLI tool. Echoes the JSON result (stdout); the engine's
181
229
  # `level=...` log lines go to stderr and are discarded unless DRAFT_MEMORY_DEBUG is set.
182
230
  # Usage: memory_cli <tool> [json-args]
@@ -32,7 +32,7 @@ EOF
32
32
 
33
33
  while [[ $# -gt 0 ]]; do
34
34
  case "$1" in
35
- --root) ROOT="$2"; shift 2;;
35
+ --root) ROOT="${2:?--root requires a value}"; shift 2;;
36
36
  --help|-h) usage; exit 0;;
37
37
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
38
38
  esac
@@ -51,8 +51,8 @@ while (($#)); do
51
51
  -h|--help) USAGE_HELP_MODE=1 usage ;;
52
52
  --enforce) ENFORCE=1; shift ;;
53
53
  --json) EMIT_JSON=1; shift ;;
54
- --caps) CAPS_CONF="$2"; shift 2 ;;
55
- --skills-dir) SKILLS_DIR="$2"; shift 2 ;;
54
+ --caps) CAPS_CONF="${2:?--caps requires a value}"; shift 2 ;;
55
+ --skills-dir) SKILLS_DIR="${2:?--skills-dir requires a value}"; shift 2 ;;
56
56
  -*) printf 'Unknown flag: %s\n' "$1" >&2; usage ;;
57
57
  *) printf 'Unexpected arg: %s\n' "$1" >&2; usage ;;
58
58
  esac
@@ -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
 
@@ -41,7 +41,7 @@ EOF
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,16 +51,9 @@ 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
@@ -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
 
@@ -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
@@ -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
60
  ARCH_JSON="$(memory_cli get_architecture "{\"project\":\"$PROJECT\",\"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"}'
@@ -54,8 +54,8 @@ EOF
54
54
 
55
55
  while [[ $# -gt 0 ]]; do
56
56
  case "$1" in
57
- --repo) REPO="$2"; shift 2;;
58
- --symbol) SYMBOL="$2"; shift 2;;
57
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
58
+ --symbol) SYMBOL="${2:?--symbol requires a value}"; shift 2;;
59
59
  --transitive) TRANSITIVE=1; shift;;
60
60
  --transitive=*) TRANSITIVE=1; DEPTH="${1#*=}"; shift;;
61
61
  --prod-only) PROD_ONLY=1; shift;;
@@ -69,20 +69,13 @@ done
69
69
  [[ -n "$SYMBOL" ]] || { echo "ERROR: --symbol is required" >&2; usage >&2; exit 1; }
70
70
  [[ "$DEPTH" =~ ^[0-9]+$ ]] || { echo "ERROR: --transitive depth must be a non-negative integer" >&2; exit 1; }
71
71
 
72
- REPO_ABS="$(cd "$REPO" && pwd)"
73
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
74
-
75
72
  unavailable() {
76
73
  jq -n --arg s "$SYMBOL" '{symbol:$s, callers:[], status:"unavailable", source:"unavailable"}' 2>/dev/null \
77
74
  || echo '{"callers":[],"status":"unavailable","source":"unavailable"}'
78
75
  exit 2
79
76
  }
80
77
 
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
78
+ graph_bootstrap "$REPO" || unavailable
86
79
 
87
80
  SYM_ESC="$(gq_escape "$SYMBOL")"
88
81
 
@@ -95,8 +88,7 @@ if [[ "$TRANSITIVE" -eq 1 ]]; then
95
88
  echo "$RES" | jq -e . >/dev/null 2>&1 || unavailable
96
89
  N="$(echo "$RES" | jq -r '(.callers // []) | length' 2>/dev/null || echo 0)"
97
90
  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
91
+ STATUS="$(gq_symbol_status "$PROJECT" "$SYM_ESC" '{"rows":[]}')"
100
92
  fi
101
93
  echo "$RES" | jq --arg s "$SYMBOL" --arg st "$STATUS" '
102
94
  {symbol:$s,
@@ -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"