@drafthq/draft 2.7.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 (158) hide show
  1. package/.claude-plugin/marketplace.json +38 -0
  2. package/.claude-plugin/plugin.json +26 -0
  3. package/LICENSE +21 -0
  4. package/README.md +272 -0
  5. package/bin/README.md +49 -0
  6. package/cli/bin/draft.js +13 -0
  7. package/cli/src/cli.js +113 -0
  8. package/cli/src/hosts/claude-code.js +46 -0
  9. package/cli/src/hosts/codex.js +33 -0
  10. package/cli/src/hosts/cursor.js +50 -0
  11. package/cli/src/hosts/index.js +24 -0
  12. package/cli/src/hosts/opencode.js +39 -0
  13. package/cli/src/installer.js +61 -0
  14. package/cli/src/lib/fsx.js +34 -0
  15. package/cli/src/lib/graph.js +23 -0
  16. package/cli/src/lib/log.js +32 -0
  17. package/cli/src/lib/paths.js +14 -0
  18. package/core/agents/architect.md +338 -0
  19. package/core/agents/debugger.md +193 -0
  20. package/core/agents/ops.md +104 -0
  21. package/core/agents/planner.md +158 -0
  22. package/core/agents/rca.md +314 -0
  23. package/core/agents/reviewer.md +256 -0
  24. package/core/agents/writer.md +110 -0
  25. package/core/guardrails/README.md +4 -0
  26. package/core/guardrails/code-quality.md +4 -0
  27. package/core/guardrails/dependency-triage.md +4 -0
  28. package/core/guardrails/design-norms.md +4 -0
  29. package/core/guardrails/language-standards.md +4 -0
  30. package/core/guardrails/review-checks.md +4 -0
  31. package/core/guardrails/secure-patterns.md +4 -0
  32. package/core/guardrails/security.md +4 -0
  33. package/core/guardrails.md +22 -0
  34. package/core/knowledge-base.md +127 -0
  35. package/core/methodology.md +1221 -0
  36. package/core/shared/condensation.md +224 -0
  37. package/core/shared/context-verify.md +44 -0
  38. package/core/shared/cross-skill-dispatch.md +127 -0
  39. package/core/shared/discovery-schema.md +75 -0
  40. package/core/shared/draft-context-loading.md +282 -0
  41. package/core/shared/git-report-metadata.md +106 -0
  42. package/core/shared/graph-query.md +239 -0
  43. package/core/shared/graph-usage-report.md +22 -0
  44. package/core/shared/jira-sync.md +170 -0
  45. package/core/shared/parallel-analysis.md +386 -0
  46. package/core/shared/parallel-fanout.md +10 -0
  47. package/core/shared/pattern-learning.md +146 -0
  48. package/core/shared/red-flags.md +58 -0
  49. package/core/shared/template-contract.md +22 -0
  50. package/core/shared/template-hygiene.md +10 -0
  51. package/core/shared/tool-resolver.md +10 -0
  52. package/core/shared/vcs-commands.md +97 -0
  53. package/core/shared/verification-gates.md +47 -0
  54. package/core/templates/CHANGELOG.md +70 -0
  55. package/core/templates/ai-context-export.md +8 -0
  56. package/core/templates/ai-context.md +270 -0
  57. package/core/templates/ai-profile.md +41 -0
  58. package/core/templates/architecture.md +203 -0
  59. package/core/templates/dependency-graph.md +103 -0
  60. package/core/templates/discovery.md +79 -0
  61. package/core/templates/guardrails.md +143 -0
  62. package/core/templates/hld.md +327 -0
  63. package/core/templates/intake-questions.md +403 -0
  64. package/core/templates/jira.md +119 -0
  65. package/core/templates/lld.md +283 -0
  66. package/core/templates/metadata.json +66 -0
  67. package/core/templates/plan.md +130 -0
  68. package/core/templates/product.md +110 -0
  69. package/core/templates/rca.md +86 -0
  70. package/core/templates/root-architecture.md +127 -0
  71. package/core/templates/root-product.md +53 -0
  72. package/core/templates/root-tech-stack.md +117 -0
  73. package/core/templates/service-index.md +55 -0
  74. package/core/templates/session-summary.md +8 -0
  75. package/core/templates/spec.md +165 -0
  76. package/core/templates/tech-matrix.md +101 -0
  77. package/core/templates/tech-stack.md +169 -0
  78. package/core/templates/track-architecture.md +311 -0
  79. package/core/templates/workflow.md +187 -0
  80. package/integrations/agents/AGENTS.md +24384 -0
  81. package/integrations/copilot/.github/copilot-instructions.md +24384 -0
  82. package/integrations/gemini/.gemini.md +26 -0
  83. package/package.json +53 -0
  84. package/scripts/fetch-memory-engine.sh +116 -0
  85. package/scripts/lib.sh +256 -0
  86. package/scripts/tools/_lib.sh +220 -0
  87. package/scripts/tools/adr-index.sh +117 -0
  88. package/scripts/tools/check-graph-usage-report.sh +95 -0
  89. package/scripts/tools/check-scope-conflicts.sh +139 -0
  90. package/scripts/tools/check-skill-line-caps.sh +115 -0
  91. package/scripts/tools/check-template-noop.sh +87 -0
  92. package/scripts/tools/check-track-hygiene.sh +230 -0
  93. package/scripts/tools/classify-files.sh +231 -0
  94. package/scripts/tools/cycle-detect.sh +75 -0
  95. package/scripts/tools/detect-test-framework.sh +135 -0
  96. package/scripts/tools/diff-templates-vs-tracks.sh +176 -0
  97. package/scripts/tools/emit-skill-metrics.sh +71 -0
  98. package/scripts/tools/fix-whitespace.sh +192 -0
  99. package/scripts/tools/freshness-check.sh +143 -0
  100. package/scripts/tools/git-metadata.sh +203 -0
  101. package/scripts/tools/graph-callers.sh +74 -0
  102. package/scripts/tools/graph-impact.sh +93 -0
  103. package/scripts/tools/graph-snapshot.sh +102 -0
  104. package/scripts/tools/hotspot-rank.sh +75 -0
  105. package/scripts/tools/manage-symlinks.sh +85 -0
  106. package/scripts/tools/mermaid-from-graph.sh +92 -0
  107. package/scripts/tools/migrate-track-frontmatter.sh +241 -0
  108. package/scripts/tools/parse-git-log.sh +135 -0
  109. package/scripts/tools/parse-reports.sh +114 -0
  110. package/scripts/tools/render-track.sh +145 -0
  111. package/scripts/tools/run-coverage.sh +153 -0
  112. package/scripts/tools/scan-markers.sh +144 -0
  113. package/scripts/tools/skill-caps.conf +24 -0
  114. package/scripts/tools/validate-frontmatter.sh +125 -0
  115. package/scripts/tools/verify-citations.sh +250 -0
  116. package/scripts/tools/verify-doc-anchors.sh +204 -0
  117. package/scripts/tools/verify-graph-binary.sh +154 -0
  118. package/skills/GRAPH.md +332 -0
  119. package/skills/adr/SKILL.md +374 -0
  120. package/skills/assist-review/SKILL.md +49 -0
  121. package/skills/bughunt/SKILL.md +668 -0
  122. package/skills/bughunt/references/regression-tests.md +399 -0
  123. package/skills/change/SKILL.md +267 -0
  124. package/skills/coverage/SKILL.md +336 -0
  125. package/skills/debug/SKILL.md +201 -0
  126. package/skills/decompose/SKILL.md +656 -0
  127. package/skills/deep-review/SKILL.md +326 -0
  128. package/skills/deploy-checklist/SKILL.md +254 -0
  129. package/skills/discover/SKILL.md +66 -0
  130. package/skills/docs/SKILL.md +42 -0
  131. package/skills/documentation/SKILL.md +197 -0
  132. package/skills/draft/SKILL.md +177 -0
  133. package/skills/draft/context-files.md +57 -0
  134. package/skills/draft/intent-mapping.md +37 -0
  135. package/skills/draft/quality-guide.md +51 -0
  136. package/skills/graph/SKILL.md +107 -0
  137. package/skills/impact/SKILL.md +86 -0
  138. package/skills/implement/SKILL.md +794 -0
  139. package/skills/incident-response/SKILL.md +245 -0
  140. package/skills/index/SKILL.md +848 -0
  141. package/skills/init/SKILL.md +1784 -0
  142. package/skills/init/references/architecture-spec.md +1259 -0
  143. package/skills/integrations/SKILL.md +53 -0
  144. package/skills/jira/SKILL.md +577 -0
  145. package/skills/jira/references/review.md +1322 -0
  146. package/skills/learn/SKILL.md +478 -0
  147. package/skills/new-track/SKILL.md +841 -0
  148. package/skills/ops/SKILL.md +57 -0
  149. package/skills/plan/SKILL.md +60 -0
  150. package/skills/quick-review/SKILL.md +216 -0
  151. package/skills/revert/SKILL.md +178 -0
  152. package/skills/review/SKILL.md +1114 -0
  153. package/skills/standup/SKILL.md +183 -0
  154. package/skills/status/SKILL.md +183 -0
  155. package/skills/tech-debt/SKILL.md +318 -0
  156. package/skills/testing-strategy/SKILL.md +195 -0
  157. package/skills/tour/SKILL.md +38 -0
  158. package/skills/upload/SKILL.md +117 -0
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env bash
2
+ # check-track-hygiene.sh
3
+ #
4
+ # Hygiene validator for Draft tracks. Enforces WS-1 contract from
5
+ # core/shared/template-hygiene.md. Generic across any project, any track,
6
+ # any domain.
7
+ #
8
+ # Checks per track:
9
+ # 1. Status parity: metadata.json:status vs Markdown "Status:" lines.
10
+ # 2. Author resolution: no Author1 / xxx@*.com / [name] placeholders.
11
+ # 3. Approver placeholders: no empty cells in Approval-bearing tables.
12
+ # 4. TBD budget: per-doc cap depends on metadata.json:status.
13
+ # 5. Plan staleness (WS-6 chain): plan.md generated_at not older than HLD/LLD.
14
+ #
15
+ # Usage:
16
+ # scripts/tools/check-track-hygiene.sh # scan all ./tracks/*
17
+ # scripts/tools/check-track-hygiene.sh tracks/foo # scan one
18
+ # scripts/tools/check-track-hygiene.sh --json ... # JSON output
19
+ #
20
+ # Exit codes:
21
+ # 0 clean
22
+ # 1 hygiene violation
23
+ # 2 usage / runtime error
24
+
25
+ set -euo pipefail
26
+
27
+ if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
28
+ echo "${0##*/} — Foundations quality tool (see core/ docs for full behavior)"
29
+ exit 0
30
+ fi
31
+
32
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
33
+ # shellcheck source=/dev/null
34
+ source "$SCRIPT_DIR/_lib.sh"
35
+
36
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
37
+
38
+ EMIT_JSON=0
39
+ TRACK_PATHS=()
40
+
41
+ usage() {
42
+ local stream=2 code=2
43
+ if [[ "${USAGE_HELP_MODE:-0}" == 1 ]]; then stream=1; code=0; fi
44
+ sed -n '2,21p' "$0" >&$stream
45
+ exit "$code"
46
+ }
47
+
48
+ while (($#)); do
49
+ case "$1" in
50
+ -h|--help) USAGE_HELP_MODE=1 usage ;;
51
+ --json) EMIT_JSON=1; shift ;;
52
+ -*) printf 'Unknown flag: %s\n' "$1" >&2; usage ;;
53
+ *) TRACK_PATHS+=("$1"); shift ;;
54
+ esac
55
+ done
56
+
57
+ if ((${#TRACK_PATHS[@]} == 0)); then
58
+ while IFS= read -r p; do TRACK_PATHS+=("$p"); done < <(discover_track_dirs "$REPO_ROOT")
59
+ fi
60
+
61
+ # Patterns
62
+ # Note: use `(example)?` rather than an empty alternation branch `(|example)` —
63
+ # strict ERE engines (BSD grep, ugrep) reject empty branches as a regex error.
64
+ FORBIDDEN_AUTHOR_RE='Author[0-9]+|xxx@(example)?\.(com|org)|\[name\]'
65
+ EMPTY_APPROVAL_ROW_RE='^\|[^|]*\|[[:space:]]*\|[[:space:]]*\|[[:space:]]*\|[[:space:]]*\|[[:space:]]*$'
66
+ TBD_RE='_TBD_[A-Za-z0-9_]+_'
67
+
68
+ violation_count=0
69
+ declare -a violations=()
70
+
71
+ record() {
72
+ local track="$1" kind="$2" file="$3" line="$4" detail="$5"
73
+ violations+=("$track|$kind|$file|$line|$detail")
74
+ violation_count=$((violation_count + 1))
75
+ }
76
+
77
+ scan_one_track() {
78
+ local track_dir="$1"
79
+ local rel_track="${track_dir#"$REPO_ROOT/"}"
80
+ local meta="$track_dir/metadata.json"
81
+
82
+ local meta_status="draft"
83
+ if [[ -f "$meta" ]]; then
84
+ local s
85
+ s="$(read_json_str "$meta" "status")"
86
+ [[ -n "$s" ]] && meta_status="$s"
87
+ fi
88
+
89
+ # 1. Status parity: search for "Status:" lines in markdown.
90
+ while IFS= read -r f; do
91
+ local rel_file="${f#"$track_dir/"}"
92
+ local n=0
93
+ while IFS= read -r line; do
94
+ n=$((n + 1))
95
+ case "$line" in
96
+ Status:*|*"**Status:**"*|*"Status: [x] Complete"*)
97
+ # Heuristic: if metadata says draft but doc says Complete, mismatch.
98
+ if [[ "$meta_status" != "completed" ]] && [[ "$line" == *'[x] Complete'* ]]; then
99
+ record "$rel_track" "status-mismatch" "$rel_file" "$n" \
100
+ "metadata.status=$meta_status, doc says Complete"
101
+ fi
102
+ ;;
103
+ esac
104
+ done < "$f"
105
+ done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
106
+
107
+ # 2. Forbidden author placeholders.
108
+ while IFS= read -r f; do
109
+ local rel_file="${f#"$track_dir/"}"
110
+ while IFS= read -r match; do
111
+ local n
112
+ n="${match%%:*}"
113
+ local rest="${match#*:}"
114
+ record "$rel_track" "forbidden-author" "$rel_file" "$n" "$rest"
115
+ done < <(grep -nE "$FORBIDDEN_AUTHOR_RE" "$f" 2>/dev/null || true)
116
+ done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
117
+
118
+ # 3. Approval-bearing tables: detect empty cells.
119
+ while IFS= read -r f; do
120
+ local rel_file="${f#"$track_dir/"}"
121
+ local in_table=0
122
+ local n=0
123
+ while IFS= read -r line; do
124
+ n=$((n + 1))
125
+ # Detect a markdown table header starting with | Role |
126
+ if echo "$line" | grep -qiE '^\|[[:space:]]*role[[:space:]]*\|'; then
127
+ in_table=1
128
+ continue
129
+ fi
130
+ # Detect end of table: blank line.
131
+ if ((in_table)) && [[ -z "$line" ]]; then
132
+ in_table=0
133
+ continue
134
+ fi
135
+ if ((in_table)) && [[ "$line" =~ $EMPTY_APPROVAL_ROW_RE ]]; then
136
+ record "$rel_track" "empty-approver" "$rel_file" "$n" "$line"
137
+ fi
138
+ done < "$f"
139
+ done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
140
+
141
+ # 4. TBD budget per-doc.
142
+ case "$meta_status" in
143
+ draft|archived) ;;
144
+ 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')
155
+ ;;
156
+ 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')
166
+ ;;
167
+ esac
168
+
169
+ # 5. Plan staleness vs HLD/LLD (WS-6).
170
+ if [[ -f "$track_dir/plan.md" ]]; then
171
+ local plan_ts hld_ts lld_ts
172
+ plan_ts="$(get_yaml_field "$track_dir/plan.md" "generated_at" || true)"
173
+ [[ -f "$track_dir/hld.md" ]] && hld_ts="$(get_yaml_field "$track_dir/hld.md" "generated_at" || true)"
174
+ [[ -f "$track_dir/lld.md" ]] && lld_ts="$(get_yaml_field "$track_dir/lld.md" "generated_at" || true)"
175
+ for sib_ts_pair in "hld.md|$hld_ts" "lld.md|$lld_ts"; do
176
+ local sib="${sib_ts_pair%%|*}"
177
+ local sib_ts="${sib_ts_pair#*|}"
178
+ [[ -z "$sib_ts" || -z "$plan_ts" ]] && continue
179
+ # Lexicographic compare works on ISO-8601 strings.
180
+ if [[ "$plan_ts" < "$sib_ts" ]]; then
181
+ record "$rel_track" "stale-plan" "plan.md" "0" \
182
+ "plan.md generated_at=$plan_ts older than $sib generated_at=$sib_ts"
183
+ fi
184
+ done
185
+ fi
186
+ }
187
+
188
+ # Self-check: git identity must be set.
189
+ if ! git config user.name >/dev/null 2>&1 || ! git config user.email >/dev/null 2>&1; then
190
+ printf 'check-track-hygiene: WARNING — git user.name/user.email not configured.\n' >&2
191
+ fi
192
+
193
+ for t in "${TRACK_PATHS[@]}"; do
194
+ [[ -d "$t" ]] || { record "$t" "not-a-directory" "" "0" ""; continue; }
195
+ scan_one_track "$(cd "$t" && pwd)"
196
+ done
197
+
198
+ emit() {
199
+ if ((EMIT_JSON)); then
200
+ printf '{"violation_count": %d, "violations": [\n' "$violation_count"
201
+ local first=1 v track kind file line detail
202
+ # Guard the expansion: "${arr[@]}" on an empty array is an unbound-variable
203
+ # error under `set -u` in bash <= 4.3 (e.g. macOS).
204
+ if ((violation_count > 0)); then
205
+ for v in "${violations[@]}"; do
206
+ IFS='|' read -r track kind file line detail <<< "$v"
207
+ if ((first)); then first=0; else printf ',\n'; fi
208
+ printf ' {"track":"%s","kind":"%s","file":"%s","line":%s,"detail":"%s"}' \
209
+ "$(json_escape "$track")" "$(json_escape "$kind")" \
210
+ "$(json_escape "$file")" "${line:-0}" "$(json_escape "$detail")"
211
+ done
212
+ fi
213
+ printf '\n]}\n'
214
+ else
215
+ if ((violation_count == 0)); then
216
+ printf 'OK: clean across %d track(s).\n' "${#TRACK_PATHS[@]}"
217
+ else
218
+ printf 'HYGIENE: %d violation(s) across %d track(s).\n' \
219
+ "$violation_count" "${#TRACK_PATHS[@]}" >&2
220
+ local v track kind file line detail
221
+ for v in "${violations[@]}"; do
222
+ IFS='|' read -r track kind file line detail <<< "$v"
223
+ printf ' [%s] %s/%s:%s — %s\n' "$kind" "$track" "$file" "$line" "$detail" >&2
224
+ done
225
+ fi
226
+ fi
227
+ }
228
+ emit
229
+
230
+ ((violation_count == 0))
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env bash
2
+ # classify-files.sh — deterministic file classification for a repo.
3
+ #
4
+ # Walks --root (default: cwd) respecting common ignore globs, emits one JSON
5
+ # object per line (JSONL) with language, category, test/generated flags,
6
+ # LOC, and top-level module.
7
+ #
8
+ # Usage:
9
+ # scripts/tools/classify-files.sh [--root DIR] [--json]
10
+ #
11
+ # Output fields: path, lang, category, is_test, is_generated, loc, module
12
+ # Exit codes: 0 OK (with or without results), 1 invocation error.
13
+ set -euo pipefail
14
+
15
+ # shellcheck source=_lib.sh
16
+ source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
17
+
18
+ ROOT="."
19
+ FORMAT="jsonl"
20
+
21
+ usage() {
22
+ cat <<'EOF'
23
+ classify-files.sh — deterministic file classification (JSONL).
24
+
25
+ Usage:
26
+ scripts/tools/classify-files.sh [--root DIR] [--json]
27
+
28
+ Flags:
29
+ --root DIR Root directory to scan (default: cwd).
30
+ --json Emit a single JSON array instead of JSONL.
31
+ --help Show this help.
32
+
33
+ Output (per file): {path, lang, category, is_test, is_generated, loc, module}
34
+
35
+ Language is inferred from extension and shebang. A file is classified as a
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.
39
+ EOF
40
+ }
41
+
42
+ while [[ $# -gt 0 ]]; do
43
+ case "$1" in
44
+ --root) ROOT="$2"; shift 2;;
45
+ --json) FORMAT="json"; shift;;
46
+ --jsonl) FORMAT="jsonl"; shift;;
47
+ --help|-h) usage; exit 0;;
48
+ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
49
+ esac
50
+ done
51
+
52
+ if [[ ! -d "$ROOT" ]]; then
53
+ echo "ERROR: --root '$ROOT' is not a directory" >&2
54
+ exit 1
55
+ fi
56
+
57
+ # Exclusions: common generated/vendor directories.
58
+ EXCLUDE_DIRS=(
59
+ .git node_modules dist build out .next .venv venv __pycache__
60
+ target vendor third_party .mypy_cache .pytest_cache coverage
61
+ .cache .tox .gradle .idea .vscode
62
+ )
63
+
64
+ lang_for() {
65
+ local path="$1"
66
+ local base; base="$(basename "$path")"
67
+ local ext="${base##*.}"
68
+ [[ "$ext" == "$base" ]] && ext=""
69
+
70
+ case "$ext" in
71
+ py) echo "python";;
72
+ js|mjs|cjs) echo "javascript";;
73
+ ts|tsx) echo "typescript";;
74
+ jsx) echo "javascript";;
75
+ go) echo "go";;
76
+ rs) echo "rust";;
77
+ java) echo "java";;
78
+ kt|kts) echo "kotlin";;
79
+ c|h) echo "c";;
80
+ cc|cpp|cxx|hpp|hh|hxx) echo "cpp";;
81
+ rb) echo "ruby";;
82
+ sh|bash) echo "bash";;
83
+ md|markdown) echo "markdown";;
84
+ yml|yaml) echo "yaml";;
85
+ json) echo "json";;
86
+ toml) echo "toml";;
87
+ proto) echo "proto";;
88
+ sql) echo "sql";;
89
+ html|htm) echo "html";;
90
+ css) echo "css";;
91
+ scss|sass) echo "sass";;
92
+ lua) echo "lua";;
93
+ php) echo "php";;
94
+ cs) echo "csharp";;
95
+ swift) echo "swift";;
96
+ *)
97
+ # shebang-based detection for extensionless scripts
98
+ if [[ -f "$path" ]] && head -1 "$path" 2>/dev/null | grep -qE '^#!'; then
99
+ local shebang
100
+ shebang="$(head -1 "$path")"
101
+ case "$shebang" in
102
+ *bash*|*zsh*|*/sh*) echo "bash";;
103
+ *python*) echo "python";;
104
+ *node*) echo "javascript";;
105
+ *ruby*) echo "ruby";;
106
+ *) echo "other";;
107
+ esac
108
+ else
109
+ echo "other"
110
+ fi
111
+ ;;
112
+ esac
113
+ }
114
+
115
+ is_test_file() {
116
+ local path="$1"
117
+ local base; base="$(basename "$path")"
118
+
119
+ # Path segment
120
+ case "/$path/" in
121
+ */tests/*|*/test/*|*/__tests__/*|*/spec/*|*/specs/*) echo "true"; return;;
122
+ esac
123
+
124
+ # Filename patterns
125
+ case "$base" in
126
+ test_*.py|*_test.py|tests.py|conftest.py) echo "true"; return;;
127
+ *_test.go) echo "true"; return;;
128
+ *.test.ts|*.test.tsx|*.test.js|*.test.jsx) echo "true"; return;;
129
+ *.spec.ts|*.spec.tsx|*.spec.js|*.spec.jsx) echo "true"; return;;
130
+ *Test.java|*Tests.java|*Spec.java) echo "true"; return;;
131
+ test-*.sh|*-test.sh|*_test.sh) echo "true"; return;;
132
+ esac
133
+
134
+ echo "false"
135
+ }
136
+
137
+ is_generated() {
138
+ local path="$1"
139
+ [[ -f "$path" ]] || { echo "false"; return; }
140
+ if head -5 "$path" 2>/dev/null | grep -qE 'Code generated|DO NOT EDIT|@generated|autogenerated'; then
141
+ echo "true"
142
+ else
143
+ echo "false"
144
+ fi
145
+ }
146
+
147
+ module_for() {
148
+ local rel="$1"
149
+ local first="${rel%%/*}"
150
+ if [[ "$first" == "$rel" ]]; then
151
+ echo "."
152
+ else
153
+ echo "$first"
154
+ fi
155
+ }
156
+
157
+ # Build find with prunes for excluded dirs. Use absolute root so emitted paths are absolute.
158
+ ROOT_REAL="$(cd "$ROOT" && pwd)"
159
+ FIND_ARGS=("$ROOT_REAL" -type d \( )
160
+ first_dir=true
161
+ for dir in "${EXCLUDE_DIRS[@]}"; do
162
+ if $first_dir; then
163
+ FIND_ARGS+=(-name "$dir")
164
+ first_dir=false
165
+ else
166
+ FIND_ARGS+=(-o -name "$dir")
167
+ fi
168
+ done
169
+ FIND_ARGS+=(\) -prune -o -type f -print)
170
+
171
+ first_record=true
172
+
173
+ emit_record() {
174
+ local json="$1"
175
+ if [[ "$FORMAT" == "json" ]]; then
176
+ if $first_record; then
177
+ printf '[\n %s' "$json"
178
+ first_record=false
179
+ else
180
+ printf ',\n %s' "$json"
181
+ fi
182
+ else
183
+ printf '%s\n' "$json"
184
+ fi
185
+ }
186
+
187
+ while IFS= read -r file; do
188
+ # `find -type f` guarantees the entry is a regular file; no re-check needed.
189
+ # `find` run on an absolute ROOT emits absolute paths — strip the prefix.
190
+ rel="${file#"$ROOT_REAL/"}"
191
+ case "$rel" in
192
+ .git/*) continue;;
193
+ esac
194
+
195
+ lang="$(lang_for "$file")"
196
+ is_test="$(is_test_file "$rel")"
197
+ gen="$(is_generated "$file")"
198
+ module="$(module_for "$rel")"
199
+
200
+ loc="$(wc -l <"$file" 2>/dev/null | tr -d ' ' || echo 0)"
201
+
202
+ # Category: test > generated > source > config/docs
203
+ if [[ "$is_test" == "true" ]]; then
204
+ category="test"
205
+ elif [[ "$gen" == "true" ]]; then
206
+ category="generated"
207
+ else
208
+ case "$lang" in
209
+ markdown|yaml|json|toml) category="docs";;
210
+ *) category="source";;
211
+ esac
212
+ fi
213
+
214
+ record=$(printf '{"path":"%s","lang":"%s","category":"%s","is_test":%s,"is_generated":%s,"loc":%s,"module":"%s"}' \
215
+ "$(json_escape "$rel")" \
216
+ "$(json_escape "$lang")" \
217
+ "$(json_escape "$category")" \
218
+ "$is_test" \
219
+ "$gen" \
220
+ "$loc" \
221
+ "$(json_escape "$module")")
222
+ emit_record "$record"
223
+ done < <(find "${FIND_ARGS[@]}" 2>/dev/null | sort)
224
+
225
+ if [[ "$FORMAT" == "json" ]]; then
226
+ if $first_record; then
227
+ printf '[]\n'
228
+ else
229
+ printf '\n]\n'
230
+ fi
231
+ fi
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ # cycle-detect.sh — emit call cycles from the knowledge graph.
3
+ #
4
+ # Backed by the codebase-memory-mcp engine. Uses bounded, fixed-length CALLS
5
+ # patterns via openCypher (this engine's dialect handles explicit patterns
6
+ # reliably but not variable-length/aggregate queries). Detects 2- and 3-node
7
+ # call cycles, which surface mutual recursion and tight coupling.
8
+ #
9
+ # Usage:
10
+ # scripts/tools/cycle-detect.sh [--repo DIR]
11
+ #
12
+ # Output: JSON {cycles:[[a,b],[a,b,c], ...], source}.
13
+ # source = "memory-graph" | "unavailable"
14
+ #
15
+ # Exit codes: 0 OK, 1 invocation error, 2 graph engine/data unavailable.
16
+ set -euo pipefail
17
+
18
+ # shellcheck source=_lib.sh
19
+ source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
20
+
21
+ REPO="."
22
+
23
+ usage() {
24
+ cat <<'EOF'
25
+ cycle-detect.sh — call-cycle detection from the knowledge graph.
26
+
27
+ Usage:
28
+ scripts/tools/cycle-detect.sh [--repo DIR]
29
+
30
+ Flags:
31
+ --repo DIR Repository root (default: cwd).
32
+ --help Show this help.
33
+
34
+ Output: JSON {cycles:[[a,b],[a,b,c]], source}. Fallback when the engine is
35
+ unavailable: {"cycles":[],"source":"unavailable"}, exit 2.
36
+ EOF
37
+ }
38
+
39
+ while [[ $# -gt 0 ]]; do
40
+ case "$1" in
41
+ --repo) REPO="$2"; shift 2;;
42
+ --help|-h) usage; exit 0;;
43
+ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
44
+ esac
45
+ done
46
+
47
+ if [[ ! -d "$REPO" ]]; then
48
+ echo "ERROR: --repo '$REPO' is not a directory" >&2
49
+ exit 1
50
+ fi
51
+
52
+ REPO_ABS="$(cd "$REPO" && pwd)"
53
+ SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
54
+
55
+ unavailable() { echo '{"cycles":[],"source":"unavailable"}'; exit 2; }
56
+
57
+ find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
58
+ command -v jq >/dev/null 2>&1 || unavailable
59
+
60
+ PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
61
+ [[ -n "$PROJECT" ]] || unavailable
62
+
63
+ # 2-cycles: a -> b -> a (dedup with a.qualified_name < b.qualified_name).
64
+ Q2="MATCH (a:Function)-[:CALLS]->(b:Function)-[:CALLS]->(a) WHERE a.qualified_name < b.qualified_name RETURN a.qualified_name AS a, b.qualified_name AS b LIMIT 100"
65
+ # 3-cycles: a -> b -> c -> a.
66
+ Q3="MATCH (a:Function)-[:CALLS]->(b:Function)-[:CALLS]->(c:Function)-[:CALLS]->(a) RETURN a.qualified_name AS a, b.qualified_name AS b, c.qualified_name AS c LIMIT 100"
67
+
68
+ R2="$(memory_cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"$Q2\"}" || echo '{}')"
69
+ R3="$(memory_cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"$Q3\"}" || echo '{}')"
70
+
71
+ jq -n --argjson r2 "${R2:-{\}}" --argjson r3 "${R3:-{\}}" '
72
+ {
73
+ cycles: (((($r2.rows) // []) + (($r3.rows) // []))),
74
+ source: "memory-graph"
75
+ }'
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env bash
2
+ # detect-test-framework.sh — detect test framework(s) used in a repo.
3
+ #
4
+ # Output: JSON {languages:[{language, framework, runner_command, test_globs, config_file}]}
5
+ #
6
+ # Detection rules (first match per language wins):
7
+ # python → pytest (pytest.ini | pyproject.toml has "[tool.pytest.ini_options]")
8
+ # | unittest (has tests but no pytest config)
9
+ # go → go test (go.mod + any *_test.go)
10
+ # javascript/typescript → vitest | jest | mocha (by package.json scripts / devDependencies)
11
+ # rust → cargo test (Cargo.toml + tests/)
12
+ # shell → plain bash (tests/ dir has test-*.sh)
13
+ #
14
+ # Usage:
15
+ # scripts/tools/detect-test-framework.sh [--root DIR]
16
+ #
17
+ # Exit codes: 0 (always; empty languages array is valid output).
18
+ set -euo pipefail
19
+
20
+ # shellcheck source=_lib.sh
21
+ source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
22
+
23
+ ROOT="."
24
+
25
+ usage() {
26
+ cat <<'EOF'
27
+ detect-test-framework.sh — detect language test frameworks.
28
+
29
+ Usage:
30
+ scripts/tools/detect-test-framework.sh [--root DIR]
31
+
32
+ Flags:
33
+ --root DIR Repository root (default: cwd).
34
+ --help Show this help.
35
+
36
+ Output: JSON {languages:[{language, framework, runner_command, test_globs, config_file}]}
37
+ EOF
38
+ }
39
+
40
+ while [[ $# -gt 0 ]]; do
41
+ case "$1" in
42
+ --root) ROOT="$2"; shift 2;;
43
+ --help|-h) usage; exit 0;;
44
+ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
45
+ esac
46
+ done
47
+
48
+ if [[ ! -d "$ROOT" ]]; then
49
+ echo "ERROR: --root '$ROOT' is not a directory" >&2
50
+ exit 1
51
+ fi
52
+
53
+ cd "$ROOT"
54
+
55
+ emit_lang() {
56
+ local language="$1" framework="$2" runner="$3" globs="$4" config="$5"
57
+ printf '{"language":"%s","framework":"%s","runner_command":"%s","test_globs":%s,"config_file":%s}' \
58
+ "$(json_escape "$language")" \
59
+ "$(json_escape "$framework")" \
60
+ "$(json_escape "$runner")" \
61
+ "$globs" \
62
+ "$([[ -z "$config" ]] && echo null || echo "\"$(json_escape "$config")\"")"
63
+ }
64
+
65
+ RESULTS=()
66
+
67
+ # ── Python ──
68
+ PY_CONFIG=""
69
+ PY_FRAMEWORK=""
70
+ if [[ -f pytest.ini ]]; then
71
+ PY_CONFIG="pytest.ini"; PY_FRAMEWORK="pytest"
72
+ elif [[ -f pyproject.toml ]] && grep -q 'tool.pytest' pyproject.toml 2>/dev/null; then
73
+ PY_CONFIG="pyproject.toml"; PY_FRAMEWORK="pytest"
74
+ elif [[ -f setup.cfg ]] && grep -qE '^\[tool:pytest\]' setup.cfg 2>/dev/null; then
75
+ PY_CONFIG="setup.cfg"; PY_FRAMEWORK="pytest"
76
+ fi
77
+ if [[ -z "$PY_FRAMEWORK" ]]; then
78
+ if find . -maxdepth 4 \( -name 'test_*.py' -o -name '*_test.py' \) \
79
+ -not -path './.git/*' -not -path './node_modules/*' -print -quit 2>/dev/null | grep -q .; then
80
+ PY_FRAMEWORK="unittest"
81
+ fi
82
+ fi
83
+ if [[ -n "$PY_FRAMEWORK" ]]; then
84
+ RESULTS+=("$(emit_lang "python" "$PY_FRAMEWORK" \
85
+ "$([[ "$PY_FRAMEWORK" == pytest ]] && echo 'pytest' || echo 'python -m unittest')" \
86
+ '["test_*.py","*_test.py"]' \
87
+ "$PY_CONFIG")")
88
+ fi
89
+
90
+ # ── Go ──
91
+ if [[ -f go.mod ]]; then
92
+ if find . -maxdepth 6 -name '*_test.go' -not -path './vendor/*' -print -quit 2>/dev/null | grep -q .; then
93
+ RESULTS+=("$(emit_lang "go" "go test" "go test ./..." '["*_test.go"]' "go.mod")")
94
+ fi
95
+ fi
96
+
97
+ # ── JS/TS ── (single file read, match in-memory)
98
+ if [[ -f package.json ]]; then
99
+ PKG_JSON="$(<package.json)"
100
+ JS_FRAMEWORK=""
101
+ if [[ "$PKG_JSON" == *'"vitest"'* ]]; then
102
+ JS_FRAMEWORK="vitest"
103
+ elif [[ "$PKG_JSON" == *'"jest"'* ]]; then
104
+ JS_FRAMEWORK="jest"
105
+ elif [[ "$PKG_JSON" == *'"mocha"'* ]]; then
106
+ JS_FRAMEWORK="mocha"
107
+ fi
108
+ if [[ -n "$JS_FRAMEWORK" ]]; then
109
+ RESULTS+=("$(emit_lang "javascript" "$JS_FRAMEWORK" \
110
+ "npm test" \
111
+ '["*.test.[jt]s","*.test.[jt]sx","*.spec.[jt]s","*.spec.[jt]sx"]' \
112
+ "package.json")")
113
+ fi
114
+ fi
115
+
116
+ # ── Rust ──
117
+ if [[ -f Cargo.toml ]]; then
118
+ RESULTS+=("$(emit_lang "rust" "cargo test" "cargo test" '["tests/*.rs","src/**/*_test.rs"]' "Cargo.toml")")
119
+ fi
120
+
121
+ # ── Shell ──
122
+ if find . -maxdepth 3 -type d -name tests -print -quit 2>/dev/null | grep -q .; then
123
+ if find tests -maxdepth 2 -name 'test-*.sh' -print -quit 2>/dev/null | grep -q .; then
124
+ RESULTS+=("$(emit_lang "shell" "bash" "./tests/run-all.sh" '["tests/test-*.sh"]' "")")
125
+ fi
126
+ fi
127
+
128
+ # Assemble
129
+ printf '{"languages":['
130
+ first=true
131
+ for r in ${RESULTS[@]+"${RESULTS[@]}"}; do
132
+ if $first; then first=false; else printf ','; fi
133
+ printf '%s' "$r"
134
+ done
135
+ printf ']}\n'