@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,250 @@
1
+ #!/usr/bin/env bash
2
+ # verify-citations.sh
3
+ #
4
+ # Resolve every `path:line` and `path:line-range` citation in a track's
5
+ # Markdown against the commit pinned in `metadata.json:synced_to_commit`,
6
+ # and confirm the cited content still resides within the line window.
7
+ #
8
+ # Drift tolerance is configurable (default ±5 lines). Citations wrapped
9
+ # inside <!-- VERIFIER:IGNORE START --> ... <!-- VERIFIER:IGNORE END -->
10
+ # blocks are skipped. Citations annotated as `(planned)` or carrying a
11
+ # `[New file ...]` annotation are skipped (must not exist yet).
12
+ #
13
+ # Usage:
14
+ # scripts/tools/verify-citations.sh # scan ./tracks/*
15
+ # scripts/tools/verify-citations.sh tracks/foo # scan one
16
+ # scripts/tools/verify-citations.sh --tolerance 10 --json tracks/foo
17
+ #
18
+ # Exit codes:
19
+ # 0 clean
20
+ # 1 drift detected
21
+ # 2 usage / runtime error
22
+
23
+ set -euo pipefail
24
+
25
+ if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
26
+ echo "${0##*/} — Foundations quality tool (see core/ docs for full behavior)"
27
+ exit 0
28
+ fi
29
+
30
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31
+ # shellcheck source=/dev/null
32
+ source "$SCRIPT_DIR/_lib.sh"
33
+
34
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
35
+
36
+ EMIT_JSON=0
37
+ TOLERANCE=5
38
+ TRACK_PATHS=()
39
+
40
+ usage() {
41
+ local stream=2 code=2
42
+ if [[ "${USAGE_HELP_MODE:-0}" == 1 ]]; then stream=1; code=0; fi
43
+ sed -n '2,20p' "$0" >&$stream
44
+ exit "$code"
45
+ }
46
+
47
+ while (($#)); do
48
+ case "$1" in
49
+ -h|--help) USAGE_HELP_MODE=1 usage ;;
50
+ --json) EMIT_JSON=1; shift ;;
51
+ --tolerance) TOLERANCE="$2"; shift 2 ;;
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
+ violation_count=0
62
+ declare -a violations=()
63
+
64
+ record() {
65
+ violations+=("$1|$2|$3|$4|$5")
66
+ violation_count=$((violation_count + 1))
67
+ }
68
+
69
+ # Extract citations from a markdown file, skipping VERIFIER:IGNORE blocks.
70
+ # Output: file<TAB>line<TAB>cite (cite is path:LINE or path:LINE-LINE).
71
+ extract_citations() {
72
+ local md="$1"
73
+ awk '
74
+ BEGIN { ignore = 0 }
75
+ /<!-- VERIFIER:IGNORE START -->/ { ignore = 1; next }
76
+ /<!-- VERIFIER:IGNORE END -->/ { ignore = 0; next }
77
+ ignore == 1 { next }
78
+ /\(planned\)|\[New file/ { next }
79
+ {
80
+ s = $0
81
+ gsub(/https?:\/\/[^[:space:]]*/, "", s)
82
+ # Use a string regex (not a /.../ literal): a literal "/" inside a
83
+ # bracket expression prematurely terminates a regex literal in
84
+ # POSIX/BSD awk ("nonterminated character class"), though gawk is
85
+ # lenient. The string form is portable across awk implementations.
86
+ while (match(s, "[A-Za-z0-9_][A-Za-z0-9_./-]*\\.[A-Za-z0-9]+:[0-9]+")) {
87
+ cite = substr(s, RSTART, RLENGTH)
88
+ printf("%s\t%d\t%s\n", FILENAME, NR, cite)
89
+ s = substr(s, RSTART + RLENGTH)
90
+ }
91
+ }
92
+ ' "$md"
93
+ }
94
+
95
+ # Resolve cite against the synced_to_commit content using git show.
96
+ # Returns 0 if match within tolerance, 1 if past-EOF, 2 if file truly missing.
97
+ #
98
+ # When the cite is a bare basename (no slash) and the literal path doesn't
99
+ # exist at the commit, fall back to a basename search across the tree — this
100
+ # accepts narrative citations like `foo.cc:42` that mean `path/to/foo.cc:42`.
101
+ # Skipped when multiple files share the basename (ambiguous → reject).
102
+ verify_one_citation() {
103
+ local repo_root="$1" commit="$2" cite="$3"
104
+ local path lo hi
105
+ path="${cite%%:*}"
106
+ local range="${cite#*:}"
107
+ if [[ "$range" == *-* ]]; then
108
+ lo="${range%%-*}"
109
+ hi="${range##*-}"
110
+ else
111
+ lo="$range"
112
+ hi="$range"
113
+ fi
114
+ path="${path#./}"
115
+
116
+ local resolved_path="$path"
117
+
118
+ # Probe via git show.
119
+ if ! git -C "$repo_root" cat-file -e "$commit":"$path" 2>/dev/null; then
120
+ # Bare-basename fallback: if the cite carries no slash, search the
121
+ # tree for files whose basename matches and accept when unique.
122
+ if [[ "$path" != */* ]]; then
123
+ local matches
124
+ matches="$(git -C "$repo_root" ls-tree -r --name-only "$commit" 2>/dev/null \
125
+ | awk -v b="$path" 'BEGIN{FS="/"} $NF==b{print}')"
126
+ local match_count
127
+ match_count="$(printf '%s\n' "$matches" | grep -c . || true)"
128
+ if (( match_count == 1 )); then
129
+ resolved_path="$matches"
130
+ elif (( match_count > 1 )); then
131
+ printf 'ambiguous-basename|%s has %d candidates: %s\n' \
132
+ "$path" "$match_count" \
133
+ "$(printf '%s' "$matches" | tr '\n' ',' | sed 's/,$//')"
134
+ return 1
135
+ else
136
+ return 2
137
+ fi
138
+ else
139
+ return 2
140
+ fi
141
+ fi
142
+
143
+ local total_lines
144
+ total_lines="$(git -C "$repo_root" show "$commit":"$resolved_path" 2>/dev/null | wc -l | tr -d ' ')"
145
+ if (( lo < 1 )); then
146
+ printf 'invalid-line|cite %s has line < 1\n' "$cite"
147
+ return 1
148
+ fi
149
+ if (( hi > total_lines + TOLERANCE )); then
150
+ printf 'past-eof|file %s has %d lines, cite %s exceeds (+tol %d)\n' \
151
+ "$resolved_path" "$total_lines" "$cite" "$TOLERANCE"
152
+ return 1
153
+ fi
154
+ if (( lo > total_lines + TOLERANCE )); then
155
+ printf 'past-eof|file %s has %d lines, cite %s start exceeds (+tol %d)\n' \
156
+ "$resolved_path" "$total_lines" "$cite" "$TOLERANCE"
157
+ return 1
158
+ fi
159
+ return 0
160
+ }
161
+
162
+ scan_one_track() {
163
+ local track_dir="$1"
164
+ local rel_track="${track_dir#"$REPO_ROOT/"}"
165
+ local meta="$track_dir/metadata.json"
166
+ local commit=""
167
+ if [[ -f "$meta" ]]; then
168
+ commit="$(read_json_str "$meta" "synced_to_commit")"
169
+ fi
170
+ if [[ -z "$commit" ]]; then
171
+ # Fall back to YAML frontmatter on spec.md if present.
172
+ [[ -f "$track_dir/spec.md" ]] && commit="$(get_yaml_field "$track_dir/spec.md" "synced_to_commit")"
173
+ fi
174
+ if [[ -z "$commit" ]]; then
175
+ record "$rel_track" "no-pinned-commit" "metadata.json" "0" \
176
+ "no synced_to_commit; cannot verify citations"
177
+ return
178
+ fi
179
+
180
+ # Determine which git repo to query. Prefer the track's enclosing repo
181
+ # (first git dir above the track_dir). Fall back to Draft repo.
182
+ local probe="$track_dir"
183
+ local target_repo=""
184
+ while [[ "$probe" != "/" ]]; do
185
+ if [[ -e "$probe/.git" ]]; then target_repo="$probe"; break; fi
186
+ probe="$(dirname "$probe")"
187
+ done
188
+ [[ -z "$target_repo" ]] && target_repo="$REPO_ROOT"
189
+
190
+ while IFS= read -r f; do
191
+ local rel_md="${f#"$track_dir/"}"
192
+ while IFS=$'\t' read -r md_file lineno cite; do
193
+ local md_line="$md_file"
194
+ local result rc=0
195
+ # set +e to capture rc; set -e would exit on a non-zero return.
196
+ set +e
197
+ result="$(verify_one_citation "$target_repo" "$commit" "$cite" 2>&1)"
198
+ rc=$?
199
+ set -e
200
+ if (( rc == 1 )); then
201
+ record "$rel_track" "cite-drift" "$rel_md" "$lineno" \
202
+ "$cite — $result"
203
+ elif (( rc == 2 )); then
204
+ record "$rel_track" "cite-missing-file" "$rel_md" "$lineno" \
205
+ "$cite — file not present at commit $commit"
206
+ fi
207
+ done < <(extract_citations "$f")
208
+ done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
209
+ }
210
+
211
+ for t in "${TRACK_PATHS[@]}"; do
212
+ [[ -d "$t" ]] || { record "$t" "not-a-directory" "" "0" ""; continue; }
213
+ scan_one_track "$(cd "$t" && pwd)"
214
+ done
215
+
216
+ emit() {
217
+ if ((EMIT_JSON)); then
218
+ printf '{"violation_count": %d, "tolerance": %d, "violations": [\n' \
219
+ "$violation_count" "$TOLERANCE"
220
+ local first=1 v track kind file line detail
221
+ # Guard the expansion: "${arr[@]}" on an empty array is an unbound-variable
222
+ # error under `set -u` in bash <= 4.3 (e.g. macOS).
223
+ if ((violation_count > 0)); then
224
+ for v in "${violations[@]}"; do
225
+ IFS='|' read -r track kind file line detail <<< "$v"
226
+ if ((first)); then first=0; else printf ',\n'; fi
227
+ printf ' {"track":"%s","kind":"%s","file":"%s","line":%s,"detail":"%s"}' \
228
+ "$(json_escape "$track")" "$(json_escape "$kind")" \
229
+ "$(json_escape "$file")" "${line:-0}" "$(json_escape "$detail")"
230
+ done
231
+ fi
232
+ printf '\n]}\n'
233
+ else
234
+ if ((violation_count == 0)); then
235
+ printf 'OK: citations clean across %d track(s) (tol=±%d).\n' \
236
+ "${#TRACK_PATHS[@]}" "$TOLERANCE"
237
+ else
238
+ printf 'CITATIONS: %d violation(s) across %d track(s) (tol=±%d).\n' \
239
+ "$violation_count" "${#TRACK_PATHS[@]}" "$TOLERANCE" >&2
240
+ local v track kind file line detail
241
+ for v in "${violations[@]}"; do
242
+ IFS='|' read -r track kind file line detail <<< "$v"
243
+ printf ' [%s] %s/%s:%s — %s\n' "$kind" "$track" "$file" "$line" "$detail" >&2
244
+ done
245
+ fi
246
+ fi
247
+ }
248
+ emit
249
+
250
+ ((violation_count == 0))
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env bash
2
+ # verify-doc-anchors.sh
3
+
4
+ #
5
+ # Verify cross-document references in a track:
6
+ # - §X.Y or §X numbered-section references → target document must contain
7
+ # a heading whose text starts with that number.
8
+ # - <doc>.md#<anchor> markdown anchors → resolve <anchor> against the
9
+ # target file's header slugs.
10
+ # - (planned) / [New file ...] annotations → file MUST NOT exist locally.
11
+ #
12
+ # Skips content inside <!-- VERIFIER:IGNORE START --> ... END --> blocks.
13
+ #
14
+ # Usage:
15
+ # scripts/tools/verify-doc-anchors.sh # scan ./tracks/*
16
+ # scripts/tools/verify-doc-anchors.sh tracks/foo # scan one
17
+ # scripts/tools/verify-doc-anchors.sh --json ...
18
+ #
19
+ # Exit codes:
20
+ # 0 clean
21
+ # 1 anchor violation
22
+ # 2 usage / runtime error
23
+
24
+ set -euo pipefail
25
+
26
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
+ # shellcheck source=/dev/null
28
+ source "$SCRIPT_DIR/_lib.sh"
29
+
30
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
31
+
32
+ EMIT_JSON=0
33
+ TRACK_PATHS=()
34
+
35
+ usage() {
36
+ local stream=2 code=2
37
+ if [[ "${USAGE_HELP_MODE:-0}" == 1 ]]; then stream=1; code=0; fi
38
+ sed -n '2,20p' "$0" >&$stream
39
+ exit "$code"
40
+ }
41
+
42
+ while (($#)); do
43
+ case "$1" in
44
+ -h|--help) USAGE_HELP_MODE=1 usage ;;
45
+ --json) EMIT_JSON=1; shift ;;
46
+ -*) printf 'Unknown flag: %s\n' "$1" >&2; usage ;;
47
+ *) TRACK_PATHS+=("$1"); shift ;;
48
+ esac
49
+ done
50
+
51
+ if ((${#TRACK_PATHS[@]} == 0)); then
52
+ while IFS= read -r p; do TRACK_PATHS+=("$p"); done < <(
53
+ find "$REPO_ROOT" -type d -path '*/tracks/*' -maxdepth 4 -mindepth 2 \
54
+ -not -path '*/.*' 2>/dev/null | sort
55
+ )
56
+ fi
57
+
58
+ violation_count=0
59
+ declare -a violations=()
60
+ record() { violations+=("$1|$2|$3|$4|$5"); violation_count=$((violation_count + 1)); }
61
+
62
+ # Build a deduplicated list of header slugs from a markdown file.
63
+ # Standard GitHub-flavored slug: lowercase, spaces → '-', strip punctuation.
64
+ md_slugs() {
65
+ awk '
66
+ /^#{1,6} +/ {
67
+ s = $0
68
+ sub(/^#+ +/, "", s)
69
+ # Strip trailing whitespace
70
+ sub(/[[:space:]]+$/, "", s)
71
+ # Lowercase
72
+ s = tolower(s)
73
+ # Replace spaces with dashes
74
+ gsub(/ +/, "-", s)
75
+ # Strip punctuation except dashes
76
+ gsub(/[^a-z0-9-]/, "", s)
77
+ print s
78
+ }
79
+ ' "$1" | sort -u
80
+ }
81
+
82
+ # Extract numbered headers (e.g. "## 7 Parallel SST transfer" or "## 20.2 ...").
83
+ md_numbered_headers() {
84
+ awk '
85
+ /^#{1,6} +[0-9]+(\.[0-9]+)*[ .]/ {
86
+ s = $0
87
+ sub(/^#+ +/, "", s)
88
+ print s
89
+ }
90
+ ' "$1"
91
+ }
92
+
93
+ scan_md() {
94
+ local track_dir="$1" md="$2"
95
+ local rel_track="${track_dir#"$REPO_ROOT/"}"
96
+ local rel_md="${md#"$track_dir/"}"
97
+ local ignore=0 lineno=0
98
+ while IFS= read -r line; do
99
+ lineno=$((lineno + 1))
100
+ if [[ "$line" == *"<!-- VERIFIER:IGNORE START -->"* ]]; then ignore=1; continue; fi
101
+ if [[ "$line" == *"<!-- VERIFIER:IGNORE END -->"* ]]; then ignore=0; continue; fi
102
+ (( ignore )) && continue
103
+
104
+ # 1. §-references inside a markdown link.
105
+ # Plain-prose `§X.Y` is too ambiguous in practice — authors commonly
106
+ # use it as shorthand for an external doc (e.g. `architecture.md §20.2`)
107
+ # without naming the file on the same line. To keep this validator
108
+ # focused on machine-verifiable cross-references, we only check
109
+ # §X.Y when it appears INSIDE a markdown link target — i.e. the
110
+ # author has structurally committed to an in-track reference.
111
+ # Example that IS validated:
112
+ # [see §3.1](./hld.md#detailed-design) ← anchor check below
113
+ # Example that is NOT validated (prose only):
114
+ # The §20.2 layout matches architecture.md.
115
+ # If a track wants stricter intra-track §-checks, it can use
116
+ # markdown links pointing to numbered headers explicitly.
117
+ : # §-ref prose validation disabled — see comment above.
118
+
119
+ # 2. Markdown anchor references like ./hld.md#section-name
120
+ if echo "$line" | grep -qE '\([^)]*\.md#[A-Za-z0-9_-]+\)'; then
121
+ local m
122
+ while IFS= read -r m; do
123
+ m="${m#(}"; m="${m%)}"
124
+ local path="${m%%#*}"
125
+ local anchor="${m##*#}"
126
+ local target
127
+ if [[ "$path" == /* ]]; then
128
+ target="$path"
129
+ else
130
+ target="$track_dir/${path#./}"
131
+ fi
132
+ if [[ ! -f "$target" ]]; then
133
+ record "$rel_track" "missing-doc" "$rel_md" "$lineno" \
134
+ "anchor target file not found: $path"
135
+ continue
136
+ fi
137
+ if ! md_slugs "$target" | grep -Fxq "$anchor"; then
138
+ record "$rel_track" "missing-anchor" "$rel_md" "$lineno" \
139
+ "$path has no slug '$anchor'"
140
+ fi
141
+ done < <(echo "$line" | grep -oE '\([^)]*\.md#[A-Za-z0-9_-]+\)')
142
+ fi
143
+
144
+ # 3. (planned) / [New file ...] annotations: the path mentioned on the
145
+ # same line must NOT yet exist relative to the track.
146
+ #
147
+ # Only fire when the line has EXACTLY one path-looking token. Lines
148
+ # with multiple paths (e.g. "Flag X (planned) — wired per `spec.md`")
149
+ # make attribution ambiguous; the validator should not guess which
150
+ # token the `(planned)` annotation owns.
151
+ if echo "$line" | grep -qE '\(planned\)|\[New file'; then
152
+ local paths
153
+ paths="$(echo "$line" | grep -oE '[A-Za-z][A-Za-z0-9_./-]*\.[A-Za-z]+' | sort -u)"
154
+ local path_count
155
+ path_count="$(printf '%s\n' "$paths" | grep -c . || true)"
156
+ if (( path_count == 1 )); then
157
+ local p="$paths"
158
+ local rel="${p#./}"
159
+ if [[ -e "$track_dir/$rel" ]]; then
160
+ record "$rel_track" "planned-file-exists" "$rel_md" "$lineno" \
161
+ "$p annotated planned but exists at $track_dir/$rel"
162
+ fi
163
+ fi
164
+ fi
165
+ done < "$md"
166
+ }
167
+
168
+ for t in "${TRACK_PATHS[@]}"; do
169
+ [[ -d "$t" ]] || { record "$t" "not-a-directory" "" "0" ""; continue; }
170
+ track_dir="$(cd "$t" && pwd)"
171
+ while IFS= read -r f; do
172
+ scan_md "$track_dir" "$f"
173
+ done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
174
+ done
175
+
176
+ emit() {
177
+ if ((EMIT_JSON)); then
178
+ printf '{"violation_count": %d, "violations": [\n' "$violation_count"
179
+ local first=1 v track kind file line detail
180
+ for v in "${violations[@]}"; do
181
+ IFS='|' read -r track kind file line detail <<< "$v"
182
+ if ((first)); then first=0; else printf ',\n'; fi
183
+ printf ' {"track":"%s","kind":"%s","file":"%s","line":%s,"detail":"%s"}' \
184
+ "$(json_escape "$track")" "$(json_escape "$kind")" \
185
+ "$(json_escape "$file")" "${line:-0}" "$(json_escape "$detail")"
186
+ done
187
+ printf '\n]}\n'
188
+ else
189
+ if ((violation_count == 0)); then
190
+ printf 'OK: anchors clean across %d track(s).\n' "${#TRACK_PATHS[@]}"
191
+ else
192
+ printf 'ANCHORS: %d violation(s) across %d track(s).\n' \
193
+ "$violation_count" "${#TRACK_PATHS[@]}" >&2
194
+ local v track kind file line detail
195
+ for v in "${violations[@]}"; do
196
+ IFS='|' read -r track kind file line detail <<< "$v"
197
+ printf ' [%s] %s/%s:%s — %s\n' "$kind" "$track" "$file" "$line" "$detail" >&2
198
+ done
199
+ fi
200
+ fi
201
+ }
202
+ emit
203
+
204
+ ((violation_count == 0))
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env bash
2
+ # verify-graph-binary.sh — validate and select the Draft knowledge-graph engine.
3
+ #
4
+ # The engine is the codebase-memory-mcp binary. Resolution order (see _lib.sh:find_memory_bin):
5
+ # 1. DRAFT_MEMORY_BIN override
6
+ # 2. codebase-memory-mcp on $PATH
7
+ # 3. Draft-managed install (~/.cache/draft/bin/)
8
+ # 4. Vendored arch-specific under bin/<arch>/
9
+ #
10
+ # Emits JSON or a human report. Exit 0 = found+usable, 2 = none (graceful for skills).
11
+ #
12
+ # Usage:
13
+ # scripts/tools/verify-graph-binary.sh [--repo <dir>] [--plugin-root <dir>] [--json] [--verbose] [--strict]
14
+ #
15
+ # --strict : fail (exit 2) when no engine is found (release/CI gates)
16
+ # Integrates with install/package and the skills/init graph step.
17
+
18
+ set -euo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ # shellcheck source=_lib.sh
22
+ source "$SCRIPT_DIR/_lib.sh"
23
+
24
+ REPO="."
25
+ PLUGIN_ROOT=""
26
+ EMIT_JSON=0
27
+ VERBOSE=0
28
+ STRICT=0
29
+
30
+ usage() {
31
+ cat <<'EOF'
32
+ verify-graph-binary.sh — Draft knowledge-graph engine resolver + verifier
33
+
34
+ Engine: codebase-memory-mcp
35
+ Resolution: DRAFT_MEMORY_BIN > PATH > ~/.cache/draft/bin > bin/<arch>/
36
+
37
+ Options:
38
+ --repo DIR Repo root for context (default .)
39
+ --plugin-root DIR Explicit Draft plugin install root
40
+ --json Emit JSON report
41
+ --verbose Human progress
42
+ --strict Exit 2 if no engine found
43
+ --help This message
44
+
45
+ Exit codes:
46
+ 0 Usable engine found and responsive to --version
47
+ 1 Bad args
48
+ 2 No engine located
49
+ EOF
50
+ }
51
+
52
+ while [[ $# -gt 0 ]]; do
53
+ case "$1" in
54
+ --repo) REPO="$2"; shift 2 ;;
55
+ --plugin-root) PLUGIN_ROOT="$2"; shift 2 ;;
56
+ --json) EMIT_JSON=1; shift ;;
57
+ --verbose) VERBOSE=1; shift ;;
58
+ --strict) STRICT=1; shift ;;
59
+ --help|-h) usage; exit 0 ;;
60
+ *) echo "Unknown: $1" >&2; usage; exit 1 ;;
61
+ esac
62
+ done
63
+
64
+ log() { [[ $VERBOSE -eq 1 ]] && echo "[verify-graph] $*" >&2 || true; }
65
+
66
+ # Resolve architecture string for the report (linux-amd64, darwin-arm64, ...).
67
+ resolve_arch() {
68
+ local os arch
69
+ os=$(uname -s | tr '[:upper:]' '[:lower:]')
70
+ arch=$(uname -m)
71
+ case "$arch" in
72
+ x86_64|amd64) arch="amd64" ;;
73
+ aarch64|arm64) arch="arm64" ;;
74
+ armv7l) arch="arm" ;;
75
+ esac
76
+ echo "${os}-${arch}"
77
+ }
78
+
79
+ ARCH="$(resolve_arch)"
80
+ log "Resolved arch: $ARCH"
81
+
82
+ REPO_ABS="$(cd "$REPO" 2>/dev/null && pwd || echo "$REPO")"
83
+ SELF_REPO="${PLUGIN_ROOT:-$SCRIPT_DIR/../..}"
84
+
85
+ # Classify the resolution source for reporting.
86
+ classify_source() {
87
+ case "$MEMORY_BIN" in
88
+ "$HOME/.cache/draft/bin/"*) echo "managed" ;;
89
+ */bin/"$ARCH"/*) echo "bundled:$ARCH" ;;
90
+ codebase-memory-mcp) echo "path" ;;
91
+ "${DRAFT_MEMORY_BIN:-__none__}") echo "override" ;;
92
+ *) echo "path" ;;
93
+ esac
94
+ }
95
+
96
+ MEMORY_BIN=""
97
+ if ! find_memory_bin "$REPO_ABS" "$SELF_REPO"; then
98
+ if [[ $EMIT_JSON -eq 1 ]]; then
99
+ local_msg="No codebase-memory-mcp engine found in DRAFT_MEMORY_BIN, PATH, ~/.cache/draft/bin, or bin/<arch>/"
100
+ if [[ $STRICT -eq 1 ]]; then
101
+ printf '{"status":"none","engine_bin":null,"source":null,"arch":"%s","message":"strict mode: %s"}\n' "$ARCH" "$local_msg"
102
+ else
103
+ printf '{"status":"unavailable","engine_bin":null,"source":null,"arch":"%s","message":"%s"}\n' "$ARCH" "$local_msg"
104
+ fi
105
+ elif [[ $STRICT -eq 1 ]]; then
106
+ echo "STRICT: No codebase-memory-mcp engine found." >&2
107
+ else
108
+ echo "ERROR: No Draft graph engine located (codebase-memory-mcp)." >&2
109
+ echo " Install it (scripts/fetch-memory-engine.sh) or put it on PATH." >&2
110
+ fi
111
+ exit 2
112
+ fi
113
+
114
+ SOURCE="$(classify_source)"
115
+ log "Selected engine: $MEMORY_BIN (source=$SOURCE)"
116
+
117
+ # Liveness: must respond to --version.
118
+ if ! "$MEMORY_BIN" --version >/dev/null 2>&1; then
119
+ log "Selected engine failed --version"
120
+ echo "ERROR: engine $MEMORY_BIN found but failed --version (wrong OS/arch or corrupt?)." >&2
121
+ if [[ $EMIT_JSON -eq 1 ]]; then
122
+ printf '{"status":"unusable","engine_bin":"%s","source":"%s","arch":"%s"}\n' \
123
+ "$(json_escape "$MEMORY_BIN")" "$(json_escape "$SOURCE")" "$ARCH"
124
+ fi
125
+ exit 2
126
+ fi
127
+
128
+ STATUS="ok"
129
+
130
+ if [[ $EMIT_JSON -eq 1 ]]; then
131
+ printf '{"status":"%s","engine_bin":"%s","source":"%s","arch":"%s"}\n' \
132
+ "$(json_escape "$STATUS")" "$(json_escape "$MEMORY_BIN")" "$(json_escape "$SOURCE")" "$(json_escape "$ARCH")"
133
+ else
134
+ echo "Draft graph engine: $MEMORY_BIN"
135
+ echo " source: $SOURCE (arch=$ARCH)"
136
+ echo " status: $STATUS"
137
+ fi
138
+
139
+ # Usage report side-effect when in a draft/ context (graph-usage-report contract).
140
+ if [[ -d "$REPO/draft" ]]; then
141
+ mkdir -p "$REPO/draft"
142
+ cat > "$REPO/draft/.graph-binary-report.json" <<EOF
143
+ {
144
+ "detected_at": "$(date -Iseconds 2>/dev/null || date)",
145
+ "engine_bin": "$(json_escape "$MEMORY_BIN")",
146
+ "source": "$(json_escape "$SOURCE")",
147
+ "arch": "$(json_escape "$ARCH")",
148
+ "status": "$(json_escape "$STATUS")"
149
+ }
150
+ EOF
151
+ log "Wrote draft/.graph-binary-report.json (usage report contract)"
152
+ fi
153
+
154
+ exit 0