@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,92 @@
1
+ #!/usr/bin/env bash
2
+ # mermaid-from-graph.sh — emit Mermaid diagrams from the knowledge graph.
3
+ #
4
+ # Backed by the codebase-memory-mcp engine. Two diagrams:
5
+ # module-deps : file co-change coupling (FILE_CHANGES_WITH edges) as a flowchart.
6
+ # proto-map : detected service routes (Route nodes) as a flowchart.
7
+ #
8
+ # When the engine is unavailable, emits an empty diagram stub and exits 2 so
9
+ # consuming skills can degrade gracefully.
10
+ #
11
+ # Usage:
12
+ # scripts/tools/mermaid-from-graph.sh [--repo DIR] [--diagram module-deps|proto-map]
13
+ #
14
+ # Exit codes: 0 OK, 1 invocation error, 2 graph engine/data unavailable.
15
+ set -euo pipefail
16
+
17
+ # shellcheck source=_lib.sh
18
+ source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
19
+
20
+ REPO="."
21
+ DIAGRAM="module-deps"
22
+
23
+ usage() {
24
+ cat <<'EOF'
25
+ mermaid-from-graph.sh — emit Mermaid diagrams from the knowledge graph.
26
+
27
+ Usage:
28
+ scripts/tools/mermaid-from-graph.sh [--repo DIR] [--diagram module-deps|proto-map]
29
+
30
+ Flags:
31
+ --repo DIR Repository root (default: cwd).
32
+ --diagram NAME module-deps (default) or proto-map.
33
+ --help Show this help.
34
+
35
+ Exit 0 with diagram output, exit 2 with an empty stub when the engine is unavailable.
36
+ EOF
37
+ }
38
+
39
+ while [[ $# -gt 0 ]]; do
40
+ case "$1" in
41
+ --repo) REPO="$2"; shift 2;;
42
+ --diagram) DIAGRAM="$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 "$REPO" ]]; then
49
+ echo "ERROR: --repo '$REPO' is not a directory" >&2
50
+ exit 1
51
+ fi
52
+
53
+ REPO_ABS="$(cd "$REPO" && pwd)"
54
+ SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
55
+
56
+ stub() {
57
+ cat <<'EOF'
58
+ ```mermaid
59
+ %% graph data unavailable — index the repo with the graph engine first
60
+ flowchart LR
61
+ empty["graph not built"]
62
+ ```
63
+ EOF
64
+ exit 2
65
+ }
66
+
67
+ find_memory_bin "$REPO_ABS" "$SELF_REPO" || stub
68
+ command -v jq >/dev/null 2>&1 || stub
69
+
70
+ PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
71
+ [[ -n "$PROJECT" ]] || stub
72
+
73
+ render_module_deps() {
74
+ local q="MATCH (a:File)-[r:FILE_CHANGES_WITH]->(b:File) RETURN a.name AS src, b.name AS dst, r.coupling_score AS score ORDER BY r.coupling_score DESC LIMIT 40"
75
+ local res; res="$(memory_cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"$q\"}" || echo '{}')"
76
+ local edges; edges="$(echo "${res:-{\}}" | jq -r '(.rows // [])[] | " \"" + (.[0]|tostring) + "\" --> \"" + (.[1]|tostring) + "\""' 2>/dev/null || true)"
77
+ if [[ -z "$edges" ]]; then return 1; fi
78
+ printf '```mermaid\nflowchart LR\n%s\n```\n' "$edges"
79
+ }
80
+
81
+ render_proto_map() {
82
+ local res; res="$(memory_cli get_architecture "{\"project\":\"$PROJECT\",\"aspects\":[\"routes\"]}" || echo '{}')"
83
+ local edges; edges="$(echo "${res:-{\}}" | jq -r '(.routes // [])[] | " \"" + ((.method // "")|tostring) + " " + ((.path // "")|tostring) + "\" --> \"" + ((.handler // "?")|tostring) + "\""' 2>/dev/null || true)"
84
+ if [[ -z "$edges" ]]; then return 1; fi
85
+ printf '```mermaid\nflowchart LR\n%s\n```\n' "$edges"
86
+ }
87
+
88
+ case "$DIAGRAM" in
89
+ module-deps) render_module_deps || stub ;;
90
+ proto-map) render_proto_map || stub ;;
91
+ *) echo "Unknown --diagram '$DIAGRAM' (expected module-deps|proto-map)" >&2; exit 1 ;;
92
+ esac
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env bash
2
+ # migrate-track-frontmatter.sh
3
+ #
4
+ # Idempotent rewriter that migrates a pre-2.0 Draft track to the WS-8
5
+ # metadata-as-source-of-truth shape:
6
+ #
7
+ # - Strips ephemeral fields from per-file YAML frontmatter in spec.md,
8
+ # hld.md, lld.md, plan.md: `git.*`, `synced_to_commit`, `classification.*`,
9
+ # `status`, `scope_includes`, `scope_excludes`.
10
+ # - Promotes them into metadata.json (creating fields if absent; preserving
11
+ # existing values).
12
+ # - Markdown frontmatter retains only: project, module, track_id,
13
+ # generated_by, generated_at, links.
14
+ #
15
+ # Idempotent: re-running on a 2.0 track produces zero diff.
16
+ # Safe: emits <file>.bak alongside each rewritten file unless --no-backup.
17
+ #
18
+ # Usage:
19
+ # scripts/tools/migrate-track-frontmatter.sh tracks/foo
20
+ # scripts/tools/migrate-track-frontmatter.sh --dry-run tracks/foo
21
+ # scripts/tools/migrate-track-frontmatter.sh --no-backup tracks/foo
22
+ #
23
+ # Exit codes:
24
+ # 0 success or no-op
25
+ # 1 migration error
26
+ # 2 usage / runtime error
27
+
28
+ set -euo pipefail
29
+
30
+ if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
31
+ echo "${0##*/} — Foundations quality tool (see core/ docs for full behavior)"
32
+ exit 0
33
+ fi
34
+
35
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
36
+ # shellcheck source=/dev/null
37
+ source "$SCRIPT_DIR/_lib.sh"
38
+
39
+ DRY_RUN=0
40
+ BACKUP=1
41
+ TRACK_DIRS=()
42
+
43
+ usage() {
44
+ local stream=2 code=2
45
+ if [[ "${USAGE_HELP_MODE:-0}" == 1 ]]; then stream=1; code=0; fi
46
+ sed -n '2,22p' "$0" >&$stream
47
+ exit "$code"
48
+ }
49
+
50
+ while (($#)); do
51
+ case "$1" in
52
+ -h|--help) USAGE_HELP_MODE=1 usage ;;
53
+ --dry-run) DRY_RUN=1; shift ;;
54
+ --no-backup) BACKUP=0; shift ;;
55
+ -*) printf 'Unknown flag: %s\n' "$1" >&2; usage ;;
56
+ *) TRACK_DIRS+=("$1"); shift ;;
57
+ esac
58
+ done
59
+
60
+ (( ${#TRACK_DIRS[@]} == 0 )) && usage
61
+
62
+ # Ephemeral keys to strip from per-file YAML frontmatter.
63
+ EPHEMERAL_KEYS=(
64
+ "git"
65
+ "synced_to_commit"
66
+ "classification"
67
+ "status"
68
+ "scope_includes"
69
+ "scope_excludes"
70
+ )
71
+
72
+ # Stable keys that survive in markdown frontmatter.
73
+ STABLE_KEYS=(
74
+ "project"
75
+ "module"
76
+ "track_id"
77
+ "generated_by"
78
+ "generated_at"
79
+ "links"
80
+ )
81
+
82
+ # Strip ephemeral blocks from a markdown file's YAML frontmatter.
83
+ # Approach: read the file; rewrite the frontmatter section so any line whose
84
+ # first token (before colon) matches an ephemeral key — plus any indented
85
+ # continuation lines — is dropped.
86
+ strip_frontmatter() {
87
+ local file="$1"
88
+ awk -v ephemeral_re="^($(IFS='|'; echo "${EPHEMERAL_KEYS[*]}"))(:|[[:space:]])" '
89
+ BEGIN { state = "before"; skip = 0 }
90
+ state == "before" {
91
+ print
92
+ if ($0 == "---") state = "in_fm"
93
+ next
94
+ }
95
+ state == "in_fm" {
96
+ if ($0 == "---") { state = "after"; print; skip = 0; next }
97
+ # Detect a top-level ephemeral key: starts at column 1.
98
+ if ($0 ~ ephemeral_re && $0 !~ /^[[:space:]]/) {
99
+ skip = 1
100
+ next
101
+ }
102
+ # Indented continuation lines belong to the previous block.
103
+ if (skip == 1 && $0 ~ /^[[:space:]]/) { next }
104
+ # Non-indented line — end of any previous ephemeral block.
105
+ skip = 0
106
+ print
107
+ next
108
+ }
109
+ state == "after" { print }
110
+ ' "$file"
111
+ }
112
+
113
+ # Promote a JSON field into metadata.json at the top level if it does not
114
+ # already exist. Prefers Python for robust JSON manipulation; falls back to
115
+ # awk for environments without Python.
116
+ #
117
+ # The awk fallback inserts before the FIRST line that is a bare "}" with no
118
+ # leading whitespace, which is the outer object's close. The previous
119
+ # implementation matched any "}" line and risked inserting fields into
120
+ # nested objects (e.g. inside `impact`).
121
+ ensure_meta_field() {
122
+ local meta="$1" key="$2" default_value="$3"
123
+ if grep -Eq "^[[:space:]]*\"$key\"[[:space:]]*:" "$meta"; then
124
+ return 0
125
+ fi
126
+ if command -v python3 >/dev/null 2>&1; then
127
+ python3 - "$meta" "$key" "$default_value" <<'PY'
128
+ import json, sys, ast, tempfile, os
129
+ path, key, raw = sys.argv[1], sys.argv[2], sys.argv[3]
130
+ with open(path) as f:
131
+ data = json.load(f)
132
+ if key in data:
133
+ sys.exit(0)
134
+ # Parse the default value: try JSON first (handles [], {}, true, numbers,
135
+ # quoted strings); fall back to literal string on failure.
136
+ try:
137
+ value = json.loads(raw)
138
+ except Exception:
139
+ value = raw.strip('"')
140
+ data[key] = value
141
+ fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path) or ".")
142
+ with os.fdopen(fd, "w") as f:
143
+ json.dump(data, f, indent=2)
144
+ f.write("\n")
145
+ os.replace(tmp, path)
146
+ PY
147
+ else
148
+ # awk fallback: insert before the LAST line that is a closing `}` at
149
+ # column 1. That is the outer object's closing brace.
150
+ awk -v key="$key" -v val="$default_value" '
151
+ { lines[NR] = $0 }
152
+ END {
153
+ # Find last bare-} line.
154
+ last_brace = 0
155
+ for (i = NR; i > 0; i--) {
156
+ if (lines[i] == "}") { last_brace = i; break }
157
+ }
158
+ for (i = 1; i <= NR; i++) {
159
+ if (i == last_brace) {
160
+ # Inject before the close.
161
+ # Ensure previous content line ends with a comma.
162
+ if (i > 1) {
163
+ prev = lines[i - 1]
164
+ sub(/[[:space:]]*$/, "", prev)
165
+ if (prev !~ /[,{[]$/) prev = prev ","
166
+ lines[i - 1] = prev
167
+ }
168
+ print " \"" key "\": " val
169
+ }
170
+ print lines[i]
171
+ }
172
+ }
173
+ ' "$meta" > "${meta}.tmp" && mv "${meta}.tmp" "$meta"
174
+ fi
175
+ }
176
+
177
+ migrate_one_track() {
178
+ local track_dir="$1"
179
+ if [[ ! -d "$track_dir" ]]; then
180
+ printf 'migrate: not a directory: %s\n' "$track_dir" >&2
181
+ return 1
182
+ fi
183
+ track_dir="$(cd "$track_dir" && pwd)"
184
+
185
+ local meta="$track_dir/metadata.json"
186
+ if [[ ! -f "$meta" ]]; then
187
+ printf 'migrate: %s has no metadata.json — creating minimal\n' "$track_dir" >&2
188
+ if (( ! DRY_RUN )); then
189
+ cat > "$meta" <<EOF
190
+ {
191
+ "id": "$(basename "$track_dir")",
192
+ "title": "_TBD_title_",
193
+ "type": "feature",
194
+ "status": "draft",
195
+ "template_version": "2.0.0",
196
+ "created": "_TBD_created_",
197
+ "updated": "_TBD_updated_",
198
+ "scope_includes": [],
199
+ "scope_excludes": [],
200
+ "phases": { "total": 0, "completed": 0 },
201
+ "tasks": { "total": 0, "completed": 0 }
202
+ }
203
+ EOF
204
+ fi
205
+ fi
206
+
207
+ if (( ! DRY_RUN )); then
208
+ ensure_meta_field "$meta" "template_version" '"2.0.0"'
209
+ ensure_meta_field "$meta" "scope_includes" '[]'
210
+ ensure_meta_field "$meta" "scope_excludes" '[]'
211
+ ensure_meta_field "$meta" "pre_deploy_status" '"unrun"'
212
+ fi
213
+
214
+ local changed=0
215
+ for f in spec.md hld.md lld.md plan.md discovery.md; do
216
+ local path="$track_dir/$f"
217
+ [[ -f "$path" ]] || continue
218
+ local before; before="$(cat "$path")"
219
+ local after; after="$(strip_frontmatter "$path")"
220
+ if [[ "$before" != "$after" ]]; then
221
+ changed=1
222
+ if (( DRY_RUN )); then
223
+ printf 'migrate: would strip ephemeral frontmatter from %s\n' "$path"
224
+ else
225
+ (( BACKUP )) && cp "$path" "$path.bak"
226
+ local _tmp; _tmp="$(mktemp "${path}.XXXXXX")"; printf '%s' "$after" > "$_tmp" && mv -f "$_tmp" "$path"
227
+ printf 'migrate: stripped ephemeral frontmatter from %s\n' "$path"
228
+ fi
229
+ fi
230
+ done
231
+
232
+ if (( changed == 0 )); then
233
+ printf 'migrate: %s already at 2.0 — no-op\n' "$track_dir"
234
+ fi
235
+ }
236
+
237
+ rc=0
238
+ for t in "${TRACK_DIRS[@]}"; do
239
+ migrate_one_track "$t" || rc=$?
240
+ done
241
+ exit "$rc"
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env bash
2
+ # parse-git-log.sh — parse conventional commits into structured JSONL.
3
+ #
4
+ # Output one JSON object per commit:
5
+ # {sha, type, scope, track_id, subject, author, timestamp, files_changed}
6
+ #
7
+ # Conventional commit subject: "type(scope): subject"
8
+ # type may end in "!" to denote breaking change.
9
+ #
10
+ # track_id:
11
+ # - extracted from the scope if it matches --scope-pattern (default none)
12
+ # - OR from the subject if a literal `[TRACK-123]` / `(TRACK-123)` appears
13
+ # - else null
14
+ #
15
+ # Usage:
16
+ # scripts/tools/parse-git-log.sh [--since RANGE] [--limit N]
17
+ # [--scope-pattern REGEX] [--branch REF]
18
+ #
19
+ # Exit codes: 0 OK, 1 invocation error.
20
+ set -euo pipefail
21
+
22
+ # shellcheck source=_lib.sh
23
+ source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
24
+
25
+ SINCE=""
26
+ LIMIT=""
27
+ SCOPE_PATTERN=""
28
+ BRANCH="HEAD"
29
+
30
+ usage() {
31
+ cat <<'EOF'
32
+ parse-git-log.sh — parse conventional commits into JSONL.
33
+
34
+ Usage:
35
+ scripts/tools/parse-git-log.sh [--since RANGE] [--limit N]
36
+ [--scope-pattern REGEX] [--branch REF]
37
+
38
+ Flags:
39
+ --since RANGE Passed to git log --since (e.g. "7d", "2 weeks ago").
40
+ --limit N Max number of commits (git log -n N).
41
+ --scope-pattern RE Extended regex; if a commit's scope matches, it becomes the track_id.
42
+ --branch REF Branch/ref to inspect (default: HEAD).
43
+ --help Show this help.
44
+
45
+ Output: JSONL with one record per commit.
46
+ Fields: sha, type, scope, track_id, subject, author, timestamp, files_changed
47
+ EOF
48
+ }
49
+
50
+ while [[ $# -gt 0 ]]; do
51
+ case "$1" in
52
+ --since) SINCE="$2"; shift 2;;
53
+ --limit) LIMIT="$2"; shift 2;;
54
+ --scope-pattern) SCOPE_PATTERN="$2"; shift 2;;
55
+ --branch) BRANCH="$2"; shift 2;;
56
+ --help|-h) usage; exit 0;;
57
+ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
58
+ esac
59
+ done
60
+
61
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
62
+ echo "ERROR: not inside a git repository" >&2
63
+ exit 1
64
+ fi
65
+
66
+ # Format: delimiter-separated metadata line, then --name-only file list, blank line separator.
67
+ GIT_ARGS=(log --pretty=tformat:'COMMIT%x1f%H%x1f%an%x1f%aI%x1f%s' --name-only --no-merges)
68
+ [[ -n "$SINCE" ]] && GIT_ARGS+=(--since="$SINCE")
69
+ [[ -n "$LIMIT" ]] && GIT_ARGS+=(-n "$LIMIT")
70
+ GIT_ARGS+=("$BRANCH")
71
+
72
+ # Read a commit block (metadata line + files) and emit one JSON record.
73
+ process_commit() {
74
+ local sha="$1" author="$2" ts="$3" subject="$4" files_changed="$5"
75
+ local type scope breaking clean_subject cc_re track_id token inner scope_val
76
+ [[ -z "$sha" ]] && return
77
+
78
+ # Parse conventional commit: type(scope)!: subject OR type: subject
79
+ type="null"
80
+ scope="null"
81
+ breaking="false"
82
+ clean_subject="$subject"
83
+ cc_re='^([a-zA-Z]+)(\(([^)]+)\))?(!)?: (.+)$'
84
+ if [[ "$subject" =~ $cc_re ]]; then
85
+ type="\"${BASH_REMATCH[1]}\""
86
+ if [[ -n "${BASH_REMATCH[3]:-}" ]]; then
87
+ scope="\"$(json_escape "${BASH_REMATCH[3]}")\""
88
+ fi
89
+ [[ -n "${BASH_REMATCH[4]:-}" ]] && breaking="true"
90
+ clean_subject="${BASH_REMATCH[5]}"
91
+ fi
92
+
93
+ # Track ID detection
94
+ track_id="null"
95
+ if [[ -n "$SCOPE_PATTERN" && "$scope" != "null" ]]; then
96
+ scope_val="${scope#?}"
97
+ scope_val="${scope_val%?}" # strip surrounding quotes
98
+ if [[ "$scope_val" =~ $SCOPE_PATTERN ]]; then
99
+ track_id="\"$(json_escape "$scope_val")\""
100
+ fi
101
+ fi
102
+ # Look for [TRACK-XXX] or (TRACK-XXX) tokens in subject
103
+ if [[ "$track_id" == "null" ]]; then
104
+ token="$(printf '%s' "$clean_subject" | grep -oE '(\[[A-Z]+-[0-9]+\]|\([A-Z]+-[0-9]+\))' | head -1 || true)"
105
+ if [[ -n "$token" ]]; then
106
+ inner="${token#?}"
107
+ inner="${inner%?}"
108
+ track_id="\"$(json_escape "$inner")\""
109
+ fi
110
+ fi
111
+
112
+ printf '{"sha":"%s","type":%s,"scope":%s,"breaking":%s,"track_id":%s,"subject":"%s","author":"%s","timestamp":"%s","files_changed":%s}\n' \
113
+ "$sha" "$type" "$scope" "$breaking" "$track_id" \
114
+ "$(json_escape "$clean_subject")" \
115
+ "$(json_escape "$author")" \
116
+ "$ts" "$files_changed"
117
+ }
118
+
119
+ # Single git log stream: each commit is `COMMIT<US>sha<US>author<US>ts<US>subject`
120
+ # followed by its file paths (one per line) and a blank separator.
121
+ cur_sha=""; cur_author=""; cur_ts=""; cur_subject=""; cur_files=0
122
+ while IFS= read -r line; do
123
+ if [[ "$line" == COMMIT$'\x1f'* ]]; then
124
+ if [[ -n "$cur_sha" ]]; then
125
+ process_commit "$cur_sha" "$cur_author" "$cur_ts" "$cur_subject" "$cur_files"
126
+ fi
127
+ IFS=$'\x1f' read -r _ cur_sha cur_author cur_ts cur_subject <<<"$line"
128
+ cur_files=0
129
+ elif [[ -n "$line" ]]; then
130
+ cur_files=$((cur_files + 1))
131
+ fi
132
+ done < <(git "${GIT_ARGS[@]}")
133
+ if [[ -n "$cur_sha" ]]; then
134
+ process_commit "$cur_sha" "$cur_author" "$cur_ts" "$cur_subject" "$cur_files"
135
+ fi
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bash
2
+ # parse-reports.sh — parse Draft reports (bughunt/review/tech-debt/...) and emit a structured summary.
3
+ #
4
+ # For each `*-report-*.md` under --root, extract YAML frontmatter fields and
5
+ # count severity markers in the report body.
6
+ #
7
+ # Output: JSON array of records:
8
+ # {path, report_type, track_id, generated_at, severity:{critical,high,medium,low,info}}
9
+ #
10
+ # Usage:
11
+ # scripts/tools/parse-reports.sh [--root DIR]
12
+ #
13
+ # Exit codes: 0 OK (even if no reports), 1 invocation error.
14
+ set -euo pipefail
15
+
16
+ # shellcheck source=_lib.sh
17
+ source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
18
+
19
+ ROOT="."
20
+
21
+ usage() {
22
+ cat <<'EOF'
23
+ parse-reports.sh — summarize Draft reports under a directory.
24
+
25
+ Usage:
26
+ scripts/tools/parse-reports.sh [--root DIR]
27
+
28
+ Flags:
29
+ --root DIR Directory to scan for *-report-*.md (default: cwd).
30
+ --help Show this help.
31
+
32
+ Output: JSON array of {path, report_type, track_id, generated_at, severity}.
33
+ EOF
34
+ }
35
+
36
+ while [[ $# -gt 0 ]]; do
37
+ case "$1" in
38
+ --root) ROOT="$2"; shift 2;;
39
+ --help|-h) usage; exit 0;;
40
+ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
41
+ esac
42
+ done
43
+
44
+ if [[ ! -d "$ROOT" ]]; then
45
+ echo "ERROR: --root '$ROOT' is not a directory" >&2
46
+ exit 1
47
+ fi
48
+
49
+ # Extract YAML frontmatter fields + severity counts in a single awk pass over each file.
50
+ # Emits tab-separated: track_id<TAB>generated_at<TAB>critical<TAB>high<TAB>medium<TAB>low<TAB>info
51
+ parse_report_fields() {
52
+ local file="$1"
53
+ awk '
54
+ BEGIN { in_fm = 0; past_fm = 0 }
55
+ NR == 1 && /^---$/ { in_fm = 1; next }
56
+ in_fm && /^---$/ { in_fm = 0; past_fm = 1; next }
57
+ in_fm {
58
+ if ($0 ~ /^track_id:[[:space:]]*/) {
59
+ v = $0; sub(/^track_id:[[:space:]]*/, "", v)
60
+ if (v ~ /^".*"$/) { v = substr(v, 2, length(v)-2) }
61
+ sub(/[[:space:]]+$/, "", v)
62
+ track_id = v
63
+ } else if ($0 ~ /^generated_at:[[:space:]]*/) {
64
+ v = $0; sub(/^generated_at:[[:space:]]*/, "", v)
65
+ if (v ~ /^".*"$/) { v = substr(v, 2, length(v)-2) }
66
+ sub(/[[:space:]]+$/, "", v)
67
+ generated_at = v
68
+ }
69
+ next
70
+ }
71
+ past_fm {
72
+ # Lowercase copy for severity detection.
73
+ l = tolower($0)
74
+ if (l ~ /(^|[^a-z])(severity:[[:space:]]*critical|\|[[:space:]]*critical[[:space:]]*\||^-[[:space:]]+critical:)/) crit++
75
+ if (l ~ /(^|[^a-z])(severity:[[:space:]]*high|\|[[:space:]]*high[[:space:]]*\||^-[[:space:]]+high:)/) high++
76
+ if (l ~ /(^|[^a-z])(severity:[[:space:]]*medium|\|[[:space:]]*medium[[:space:]]*\||^-[[:space:]]+medium:)/) med++
77
+ if (l ~ /(^|[^a-z])(severity:[[:space:]]*low|\|[[:space:]]*low[[:space:]]*\||^-[[:space:]]+low:)/) low++
78
+ if (l ~ /(^|[^a-z])(severity:[[:space:]]*info|\|[[:space:]]*info[[:space:]]*\||^-[[:space:]]+info:)/) info++
79
+ }
80
+ END {
81
+ printf "%s\t%s\t%d\t%d\t%d\t%d\t%d", track_id, generated_at, crit+0, high+0, med+0, low+0, info+0
82
+ }
83
+ ' "$file"
84
+ }
85
+
86
+ first=true
87
+ printf '['
88
+ while IFS= read -r -d '' file; do
89
+ base="$(basename "$file")"
90
+ report_type=""
91
+ if [[ "$base" =~ ^([a-z][a-z0-9-]+)-report- ]]; then
92
+ report_type="${BASH_REMATCH[1]}"
93
+ fi
94
+
95
+ fields="$(parse_report_fields "$file")"
96
+ IFS=$'\t' read -r track_id generated_at crit high med low info <<<"$fields" || true
97
+ [[ "$track_id" == "null" ]] && track_id=""
98
+
99
+ rel="${file#"$ROOT/"}"
100
+
101
+ if $first; then first=false; else printf ','; fi
102
+ printf '\n {"path":"%s","report_type":"%s","track_id":%s,"generated_at":"%s","severity":{"critical":%s,"high":%s,"medium":%s,"low":%s,"info":%s}}' \
103
+ "$(json_escape "$rel")" \
104
+ "$(json_escape "$report_type")" \
105
+ "$([[ -n "$track_id" ]] && echo "\"$(json_escape "$track_id")\"" || echo "null")" \
106
+ "$(json_escape "$generated_at")" \
107
+ "${crit:-0}" "${high:-0}" "${med:-0}" "${low:-0}" "${info:-0}"
108
+ done < <(find "$ROOT" -type f -name '*-report-*.md' -print0 2>/dev/null | sort -z)
109
+
110
+ if $first; then
111
+ printf ']\n'
112
+ else
113
+ printf '\n]\n'
114
+ fi