@drafthq/draft 3.2.1 → 3.3.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 (31) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +28 -0
  4. package/README.md +2 -2
  5. package/cli/src/hosts/cursor.js +35 -5
  6. package/cli/src/installer.js +12 -0
  7. package/cli/src/lib/cursor-registry.js +122 -0
  8. package/cli/src/lib/plugin-manifest.js +20 -0
  9. package/core/methodology.md +1 -1
  10. package/core/templates/okf/ai-context-index.md +48 -0
  11. package/core/templates/okf/concept.md +54 -0
  12. package/core/templates/okf/index.md +40 -0
  13. package/core/templates/okf/section-index.md +25 -0
  14. package/integrations/agents/AGENTS.md +447 -1
  15. package/integrations/copilot/.github/copilot-instructions.md +447 -1
  16. package/package.json +3 -2
  17. package/scripts/lib.sh +9 -0
  18. package/scripts/tools/graph-preflight.sh +259 -0
  19. package/scripts/tools/okf-render-views.sh +373 -0
  20. package/scripts/tools/okf-validate.sh +204 -0
  21. package/skills/init/SKILL.md +19 -0
  22. package/skills/init/references/okf-emitter.md +223 -0
  23. package/integrations/copilot/.github/copilot-instructions.md.7iDz8X +0 -91
  24. package/integrations/copilot/.github/copilot-instructions.md.DoBdtd +0 -91
  25. package/integrations/copilot/.github/copilot-instructions.md.McGoBW +0 -122
  26. package/integrations/copilot/.github/copilot-instructions.md.VsPyLB +0 -91
  27. package/integrations/copilot/.github/copilot-instructions.md.XAVr7D +0 -91
  28. package/integrations/copilot/.github/copilot-instructions.md.YoFVFa +0 -91
  29. package/integrations/copilot/.github/copilot-instructions.md.a9DeW0 +0 -91
  30. package/integrations/copilot/.github/copilot-instructions.md.oxQs3B +0 -91
  31. package/integrations/copilot/.github/copilot-instructions.md.ww33Ly +0 -91
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env bash
2
+ # graph-preflight.sh — read-only go/no-go check before indexing a repo with the
3
+ # Draft knowledge-graph engine (codebase-memory-mcp).
4
+ #
5
+ # Indexes NOTHING. Walks git metadata + engine status only. Safe to run anywhere.
6
+ # Companion preflight for `scripts/tools/graph-init.sh` / `/draft:init --graph-only`.
7
+ #
8
+ # Usage: scripts/tools/graph-preflight.sh [--json] [REPO_PATH] (default repo: cwd)
9
+ # --json emit a machine-readable report on stdout (no human output)
10
+ # Exit: 0 = GO / GO-with-caution, 1 = NO-GO (blocking), 2 = bad invocation.
11
+ #
12
+ # Deliberately uses guard idioms (`|| true`, `|| echo 0`) rather than aborting:
13
+ # the report accumulates blockers/warnings and prints a verdict even under -e.
14
+ set -euo pipefail
15
+
16
+ TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ SELF_REPO="$(cd "$TOOLS_DIR/../.." && pwd)"
18
+ # shellcheck source=_lib.sh
19
+ source "$TOOLS_DIR/_lib.sh"
20
+
21
+ usage() {
22
+ cat <<'EOF'
23
+ graph-preflight.sh — read-only go/no-go check before knowledge-graph indexing.
24
+
25
+ Usage:
26
+ scripts/tools/graph-preflight.sh [--json] [REPO_PATH] (default repo: cwd)
27
+
28
+ Flags:
29
+ --json Emit a machine-readable report on stdout (no human output).
30
+ --help Show this help.
31
+
32
+ Indexes nothing — walks git metadata + engine status only.
33
+ Exit codes: 0 GO / GO-with-caution, 1 NO-GO (blocking), 2 bad invocation.
34
+ EOF
35
+ }
36
+
37
+ # --- args ---
38
+ JSON_MODE=0
39
+ REPO=""
40
+ while [[ $# -gt 0 ]]; do
41
+ case "$1" in
42
+ --json) JSON_MODE=1; shift;;
43
+ -h|--help) usage; exit 0;;
44
+ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 2;;
45
+ *) if [[ -z "$REPO" ]]; then REPO="$1"; else echo "Unexpected arg: $1" >&2; exit 2; fi; shift;;
46
+ esac
47
+ done
48
+ REPO="${REPO:-.}"
49
+ [[ -d "$REPO" ]] || { echo "ERROR: '$REPO' is not a directory" >&2; exit 2; }
50
+ REPO_ABS="$(cd "$REPO" && pwd)"
51
+
52
+ # --- formatting (color only on a tty, and never in --json) ---
53
+ if [[ -t 1 && "$JSON_MODE" -eq 0 ]]; then B=$'\e[1m'; G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; D=$'\e[0m'; else B=""; G=""; Y=""; R=""; D=""; fi
54
+ hr() { [[ "$JSON_MODE" -eq 0 ]] && printf '%s\n' "------------------------------------------------------------"; return 0; }
55
+ sec() { [[ "$JSON_MODE" -eq 0 ]] && { echo; printf '%s== %s ==%s\n' "$B" "$1" "$D"; }; return 0; }
56
+ ok() { [[ "$JSON_MODE" -eq 0 ]] && printf ' %s[ OK ]%s %s\n' "$G" "$D" "$1"; return 0; }
57
+ info() { [[ "$JSON_MODE" -eq 0 ]] && printf ' %s\n' "$1"; return 0; }
58
+ warn() { WARNINGS=$((WARNINGS+1)); WARN_J="${WARN_J:+$WARN_J,}\"$(json_escape "$1")\""; [[ "$JSON_MODE" -eq 0 ]] && printf ' %s[WARN]%s %s\n' "$Y" "$D" "$1"; return 0; }
59
+ fail() { BLOCKERS=$((BLOCKERS+1)); FAIL_J="${FAIL_J:+$FAIL_J,}\"$(json_escape "$1")\""; [[ "$JSON_MODE" -eq 0 ]] && printf ' %s[FAIL]%s %s\n' "$R" "$D" "$1"; return 0; }
60
+
61
+ WARNINGS=0; BLOCKERS=0; WARN_J=""; FAIL_J=""
62
+
63
+ # --- helpers ---
64
+ count_files() { { git -C "$REPO_ABS" ls-files -- "$@" 2>/dev/null || true; } | wc -l | tr -d ' '; }
65
+ count_loc() {
66
+ [[ -n "$(git -C "$REPO_ABS" ls-files -- "$@" 2>/dev/null | head -1)" ]] || { echo 0; return; }
67
+ # cat must run from the repo root so tracked paths resolve.
68
+ ( cd "$REPO_ABS" && git ls-files -z -- "$@" 2>/dev/null | xargs -0 cat 2>/dev/null ) | wc -l | tr -d ' ' || true
69
+ }
70
+ human() { awk -v n="$1" 'BEGIN{ v=n; split("K M B",u); if(v<1000){printf "%d",v;exit}
71
+ for(i=1;i<=3;i++){v/=1000; if(v<1000){printf "%.1f%s",v,u[i];exit}}}'; }
72
+
73
+ # --- collected fields (defaults so --json is always well-formed) ---
74
+ IS_GIT=false; AT_ROOT=false; GIT_TOP=""; COMMIT="none"
75
+ TRACKED=0; ALLDISK=0; TOTAL_LOC=0; CCGO_LOC=0; LANG_J=""
76
+ VEND_J=""; ENGINE=""; VER=""; LIMIT=""; ENGINE_FOUND=false
77
+ RAM_GB=""; FREE_GB=""
78
+
79
+ hr
80
+ [[ "$JSON_MODE" -eq 0 ]] && printf '%sDraft graph pre-flight%s — %s\n' "$B" "$D" "$REPO_ABS" || true
81
+ hr
82
+
83
+ # ============================================================
84
+ sec "1. Git boundary"
85
+ # ============================================================
86
+ GIT_TOP="$(git -C "$REPO_ABS" rev-parse --show-toplevel 2>/dev/null || true)"
87
+ if [[ -z "$GIT_TOP" ]]; then
88
+ fail "Not inside a git repo. The engine would raw-walk the filesystem (no .gitignore filter)."
89
+ info "Point this at a real git repo root, never a parent container dir."
90
+ GIT_OK=0
91
+ else
92
+ GIT_OK=1; IS_GIT=true
93
+ if [[ "$GIT_TOP" != "$REPO_ABS" ]]; then
94
+ warn "Not at the git root. Git top is: $GIT_TOP"
95
+ info "Run /draft:init --graph-only at the git root for whole-repo coverage."
96
+ else
97
+ AT_ROOT=true
98
+ ok "Git root: $GIT_TOP"
99
+ fi
100
+ COMMIT="$(git -C "$REPO_ABS" rev-parse --short HEAD 2>/dev/null || echo none)"
101
+ info "HEAD: $COMMIT"
102
+ [[ -f "$GIT_TOP/.gitmodules" ]] && warn "Submodules present — engine indexes the superproject's tracked tree; submodule contents may need separate indexing." || true
103
+ fi
104
+
105
+ # ============================================================
106
+ sec "2. Index scope (git-tracked = what actually gets indexed)"
107
+ # ============================================================
108
+ if [[ "$GIT_OK" -eq 1 ]]; then
109
+ TRACKED="$(count_files)"
110
+ ALLDISK="$({ find "$REPO_ABS" -type d -name .git -prune -o -type f -print 2>/dev/null || true; } | wc -l | tr -d ' ')"
111
+ ok "Git-tracked files: $TRACKED (on disk: $ALLDISK — the difference is gitignored and SKIPPED)"
112
+
113
+ [[ "$JSON_MODE" -eq 0 ]] && { echo; printf ' %-14s %10s %12s\n' "language" "files" "lines"; printf ' %-14s %10s %12s\n' "--------" "-----" "-----"; } || true
114
+ declare -A GLOBS=(
115
+ [C/C++]='*.c *.cc *.cpp *.cxx *.h *.hpp *.hh *.hxx'
116
+ [Go]='*.go'
117
+ [Python]='*.py'
118
+ [TS/JS]='*.ts *.tsx *.js *.jsx *.mjs'
119
+ [Rust]='*.rs'
120
+ [Java]='*.java'
121
+ )
122
+ for lang in "C/C++" Go Python TS/JS Rust Java; do
123
+ # shellcheck disable=SC2086
124
+ read -ra g <<< "${GLOBS[$lang]}"
125
+ f="$(count_files "${g[@]}")"
126
+ [[ "$f" -eq 0 ]] && continue
127
+ l="$(count_loc "${g[@]}")"
128
+ [[ "$JSON_MODE" -eq 0 ]] && printf ' %-14s %10s %12s\n' "$lang" "$f" "$l" || true
129
+ LANG_J="${LANG_J:+$LANG_J,}{\"lang\":\"$(json_escape "$lang")\",\"files\":$f,\"lines\":$l}"
130
+ TOTAL_LOC=$((TOTAL_LOC + l))
131
+ [[ "$lang" == "C/C++" || "$lang" == "Go" ]] && CCGO_LOC=$((CCGO_LOC + l)) || true
132
+ done
133
+ [[ "$JSON_MODE" -eq 0 ]] && printf ' %-14s %10s %12s\n' "TOTAL" "$TRACKED" "$TOTAL_LOC" || true
134
+ info "Source LOC total: $(human "$TOTAL_LOC") | C/C++/Go: $(human "$CCGO_LOC")"
135
+ else
136
+ warn "Skipped — no git repo."
137
+ fi
138
+
139
+ # ============================================================
140
+ sec "3. Committed vendor/generated trees (these WILL be indexed)"
141
+ # ============================================================
142
+ if [[ "$GIT_OK" -eq 1 ]]; then
143
+ # Match vendor/generated *directories* (token followed by /) and protobuf-generated
144
+ # file suffixes — not filenames that merely contain "gen"/"generate".
145
+ VEND="$(git -C "$REPO_ABS" ls-files 2>/dev/null \
146
+ | grep -iE '(^|/)(third_party|thirdparty|vendor|external|deps|generated)/|\.pb\.(cc|h|go)$|_pb2\.py$' \
147
+ | sed -E 's#(^.*/(third_party|thirdparty|vendor|external|deps|generated))/.*#\1/#' \
148
+ | sort -u | head -40 || true)"
149
+ if [[ -n "$VEND" ]]; then
150
+ warn "Committed vendor/generated paths found — gitignore to exclude, or accept index inflation:"
151
+ while IFS= read -r p; do
152
+ [[ -z "$p" ]] && continue
153
+ info "$p"
154
+ VEND_J="${VEND_J:+$VEND_J,}\"$(json_escape "$p")\""
155
+ done <<< "$VEND"
156
+ else
157
+ ok "No obvious committed vendor/generated trees."
158
+ fi
159
+ else
160
+ warn "Skipped — no git repo."
161
+ fi
162
+
163
+ # ============================================================
164
+ sec "4. Engine availability"
165
+ # ============================================================
166
+ if find_memory_bin "$REPO_ABS" "$SELF_REPO"; then
167
+ ENGINE="$MEMORY_BIN"
168
+ ENGINE_FOUND=true
169
+ VER="$("$ENGINE" --version 2>/dev/null | head -1 || echo '?')"
170
+ ok "Engine: $ENGINE ($VER)"
171
+ LIMIT="$("$ENGINE" config list 2>/dev/null | awk '/auto_index_limit/{print $3}' || true)"
172
+ [[ -n "$LIMIT" ]] && info "auto_index_limit: $LIMIT (governs AUTO-index only; explicit index_repository should bypass)" || true
173
+ if [[ "$GIT_OK" -eq 1 && -n "${LIMIT:-}" && "$TRACKED" -gt "$LIMIT" ]]; then
174
+ warn "Tracked files ($TRACKED) > auto_index_limit ($LIMIT) — confirm the explicit index isn't truncated near $LIMIT."
175
+ fi
176
+ else
177
+ ENGINE=""
178
+ fail "Engine 'codebase-memory-mcp' not found (checked \$DRAFT_MEMORY_BIN, PATH, ~/.cache/draft/bin/, vendored bin/<arch>/)."
179
+ info "Install: scripts/fetch-memory-engine.sh (or put the binary on PATH)"
180
+ fi
181
+
182
+ # ============================================================
183
+ sec "5. Machine headroom"
184
+ # ============================================================
185
+ if [[ -r /proc/meminfo ]]; then
186
+ RAM_GB="$(awk '/MemTotal/{printf "%d", $2/1024/1024}' /proc/meminfo)"
187
+ ok "Total RAM: ${RAM_GB} GB (engine self-budgets ~half)"
188
+ elif command -v sysctl >/dev/null 2>&1; then
189
+ RAM_GB="$(( $(sysctl -n hw.memsize 2>/dev/null || echo 0) / 1024 / 1024 / 1024 ))"
190
+ ok "Total RAM: ${RAM_GB} GB"
191
+ else
192
+ warn "Could not read total RAM."
193
+ fi
194
+ CACHE_DIR="$HOME/.cache"; mkdir -p "$CACHE_DIR" 2>/dev/null || true
195
+ FREE_K="$(df -Pk "$CACHE_DIR" 2>/dev/null | awk 'NR==2{print $4}' || true)"
196
+ if [[ -n "${FREE_K:-}" ]]; then
197
+ FREE_GB=$((FREE_K / 1024 / 1024))
198
+ if [[ "$FREE_GB" -lt 10 ]]; then warn "$CACHE_DIR free: ${FREE_GB} GB (low — index lives here)"; else ok "$CACHE_DIR free: ${FREE_GB} GB"; fi
199
+ fi
200
+
201
+ # ============================================================
202
+ # Scale heuristic for first-pass time expectation.
203
+ if [[ "$CCGO_LOC" -ge 5000000 || "$TOTAL_LOC" -ge 5000000 ]]; then
204
+ warn "Large codebase ($(human "$TOTAL_LOC") LOC) — expect a long first-pass index (likely hours). Run backgrounded; incremental thereafter."
205
+ fi
206
+
207
+ # --- verdict ---
208
+ if [[ "$BLOCKERS" -gt 0 ]]; then VERDICT="NO_GO"; VEXIT=1
209
+ elif [[ "$WARNINGS" -gt 0 ]]; then VERDICT="GO_WITH_CAUTION"; VEXIT=0
210
+ else VERDICT="GO"; VEXIT=0
211
+ fi
212
+
213
+ # ============================================================
214
+ # Output
215
+ # ============================================================
216
+ if [[ "$JSON_MODE" -eq 1 ]]; then
217
+ printf '{\n'
218
+ printf ' "repo": "%s",\n' "$(json_escape "$REPO_ABS")"
219
+ printf ' "is_git_repo": %s,\n' "$IS_GIT"
220
+ printf ' "git_root": %s,\n' "$([[ -n "$GIT_TOP" ]] && printf '"%s"' "$(json_escape "$GIT_TOP")" || printf 'null')"
221
+ printf ' "at_git_root": %s,\n' "$AT_ROOT"
222
+ printf ' "head": "%s",\n' "$(json_escape "$COMMIT")"
223
+ printf ' "tracked_files": %s,\n' "$TRACKED"
224
+ printf ' "files_on_disk": %s,\n' "$ALLDISK"
225
+ printf ' "languages": [%s],\n' "$LANG_J"
226
+ printf ' "total_source_loc": %s,\n' "$TOTAL_LOC"
227
+ printf ' "ccgo_loc": %s,\n' "$CCGO_LOC"
228
+ printf ' "committed_vendor_paths": [%s],\n' "$VEND_J"
229
+ printf ' "engine": {"found": %s, "path": %s, "version": %s, "auto_index_limit": %s},\n' \
230
+ "$ENGINE_FOUND" \
231
+ "$([[ -n "$ENGINE" ]] && printf '"%s"' "$(json_escape "$ENGINE")" || printf 'null')" \
232
+ "$([[ -n "$VER" ]] && printf '"%s"' "$(json_escape "$VER")" || printf 'null')" \
233
+ "${LIMIT:-null}"
234
+ printf ' "machine": {"ram_gb": %s, "cache_free_gb": %s},\n' "${RAM_GB:-null}" "${FREE_GB:-null}"
235
+ printf ' "warnings": [%s],\n' "$WARN_J"
236
+ printf ' "blockers": [%s],\n' "$FAIL_J"
237
+ printf ' "verdict": "%s",\n' "$VERDICT"
238
+ printf ' "exit_code": %s\n' "$VEXIT"
239
+ printf '}\n'
240
+ exit "$VEXIT"
241
+ fi
242
+
243
+ sec "Verdict"
244
+ echo
245
+ case "$VERDICT" in
246
+ NO_GO) printf '%s NO-GO %s — %d blocker(s), %d warning(s). Resolve blockers above first.\n' "$R" "$D" "$BLOCKERS" "$WARNINGS";;
247
+ GO_WITH_CAUTION) printf '%s GO (with caution) %s — %d warning(s). Review them, then proceed.\n' "$Y" "$D" "$WARNINGS";;
248
+ GO) printf '%s GO %s — clear to index.\n' "$G" "$D";;
249
+ esac
250
+
251
+ cat <<EOF
252
+
253
+ Next step (when ready, from the git root):
254
+ scripts/tools/graph-init.sh --scope . --json & # or: /draft:init --graph-only
255
+ ${ENGINE:-codebase-memory-mcp} cli list_projects '{}'
256
+ ${ENGINE:-codebase-memory-mcp} cli index_status '{"project":"<name>"}'
257
+ EOF
258
+ hr
259
+ exit "$VEXIT"
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env bash
2
+ # okf-render-views.sh — render the demoted views from an OKF taxonomy bundle.
3
+ #
4
+ # The wiki/ bundle is the source of truth. This produces the two derived,
5
+ # human-facing views deterministically (so they never drift from the bundle and
6
+ # carry zero extra maintenance):
7
+ # 1. architecture.md — a single linear concatenation of every concept page,
8
+ # frontmatter stripped, in canonical section order, with a banner + TOC.
9
+ # This is the onboarding "read one doc" view (demoted, not deleted).
10
+ # 2. Concept Map — a routing table injected between the
11
+ # <!-- CONCEPT-MAP:START --> / <!-- CONCEPT-MAP:END --> markers in
12
+ # wiki/index.md (and optionally another index-root file).
13
+ #
14
+ # Usage:
15
+ # okf-render-views.sh <BUNDLE_DIR> --arch-out <FILE> [--concept-map-into <FILE>]
16
+ #
17
+ # BUNDLE_DIR is the wiki/ directory. Exit 0 ok, 1 error, 2 bundle not found.
18
+ set -euo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ # shellcheck source=scripts/tools/_lib.sh
22
+ source "$SCRIPT_DIR/_lib.sh"
23
+
24
+ BUNDLE=""
25
+ ARCH_OUT=""
26
+ WEB_OUT=""
27
+ CMAP_INTO=()
28
+
29
+ usage() {
30
+ cat <<'EOF'
31
+ okf-render-views.sh — render architecture.md + Concept Map + HTML viewer from an OKF bundle.
32
+
33
+ Usage:
34
+ okf-render-views.sh <BUNDLE_DIR> [--arch-out FILE] [--concept-map-into FILE]... [--web FILE]
35
+
36
+ Flags:
37
+ --arch-out FILE Write the rendered linear architecture.md here.
38
+ --concept-map-into FILE Inject the Concept Map between the CONCEPT-MAP markers
39
+ in FILE (repeatable: e.g. wiki/index.md and ai-context.md).
40
+ --web FILE Write a self-contained, offline HTML viewer (single file:
41
+ all pages inlined, built-in markdown renderer, sidebar +
42
+ search). Double-click to open — no server, no internet.
43
+ --help Show this help.
44
+
45
+ Requires jq (already a Draft prereq) for --web. Exit 0 ok, 1 error, 2 bundle not found.
46
+ EOF
47
+ }
48
+
49
+ while [[ $# -gt 0 ]]; do
50
+ case "$1" in
51
+ --arch-out) ARCH_OUT="$2"; shift 2;;
52
+ --concept-map-into) CMAP_INTO+=("$2"); shift 2;;
53
+ --web) WEB_OUT="$2"; shift 2;;
54
+ --help|-h) usage; exit 0;;
55
+ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
56
+ *)
57
+ if [[ -z "$BUNDLE" ]]; then BUNDLE="$1"; else echo "Unexpected arg: $1" >&2; exit 1; fi
58
+ shift
59
+ ;;
60
+ esac
61
+ done
62
+
63
+ [[ -n "$BUNDLE" ]] || { usage >&2; exit 1; }
64
+ [[ -d "$BUNDLE" ]] || { echo "ERROR: bundle directory not found: $BUNDLE" >&2; exit 2; }
65
+ BUNDLE="${BUNDLE%/}"
66
+
67
+ # Canonical section order for the linear render. Sections not present are skipped.
68
+ SECTIONS=(overview systems features reference entrypoints)
69
+
70
+ # Emit bundle-relative page paths in canonical order: for each section, its
71
+ # index.md first, then the rest alphabetically. Pages outside these sections
72
+ # (e.g. log.md, the bundle root index.md) are excluded from the linear view.
73
+ ordered_pages() {
74
+ local sec dir f
75
+ for sec in "${SECTIONS[@]}"; do
76
+ dir="$BUNDLE/$sec"
77
+ [[ -d "$dir" ]] || continue
78
+ [[ -f "$dir/index.md" ]] && echo "$sec/index.md"
79
+ while IFS= read -r f; do
80
+ [[ "$(basename "$f")" == "index.md" ]] && continue
81
+ echo "$sec/${f##*/}"
82
+ done < <(find "$dir" -maxdepth 1 -type f -name '*.md' | sort)
83
+ done
84
+ }
85
+
86
+ # Strip YAML frontmatter from a page (leading --- ... --- block on line 1).
87
+ strip_frontmatter() {
88
+ awk '
89
+ NR==1 && /^---$/ { fm=1; next }
90
+ fm && /^---$/ { fm=0; next }
91
+ !fm { print }
92
+ ' "$1"
93
+ }
94
+
95
+ # --- 1. Render architecture.md ---
96
+ render_architecture() {
97
+ local out="$1"
98
+ local tmp; tmp="$(mktemp)"
99
+ {
100
+ echo "---"
101
+ echo "generated_by: \"draft:init (okf-render-views.sh)\""
102
+ echo "view: rendered"
103
+ echo "source_of_truth: \"wiki/\""
104
+ echo "---"
105
+ echo ""
106
+ echo "# Architecture (Rendered View)"
107
+ echo ""
108
+ echo "> **Generated** from the \`wiki/\` OKF bundle — do not edit by hand."
109
+ echo "> The bundle is the source of truth; this is the single-document linear"
110
+ echo "> view for onboarding. Regenerate with \`okf-render-views.sh\`."
111
+ echo ""
112
+ echo "## Contents"
113
+ echo ""
114
+ # TOC from page titles.
115
+ local rel title sec last_sec=""
116
+ while IFS= read -r rel; do
117
+ [[ -z "$rel" ]] && continue
118
+ sec="${rel%%/*}"
119
+ if [[ "$sec" != "$last_sec" ]]; then
120
+ echo "- **${sec}/**"
121
+ last_sec="$sec"
122
+ fi
123
+ title="$(get_yaml_field "$BUNDLE/$rel" title)"
124
+ [[ -n "$title" ]] || title="$rel"
125
+ local anchor; anchor="$(printf '%s' "$title" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-')"
126
+ anchor="${anchor#-}"; anchor="${anchor%-}"
127
+ echo " - [${title}](#${anchor})"
128
+ done < <(ordered_pages)
129
+ echo ""
130
+ # Body: each page, frontmatter stripped.
131
+ while IFS= read -r rel; do
132
+ [[ -z "$rel" ]] && continue
133
+ echo ""
134
+ echo "---"
135
+ echo ""
136
+ strip_frontmatter "$BUNDLE/$rel"
137
+ done < <(ordered_pages)
138
+ } >"$tmp"
139
+ mv "$tmp" "$out"
140
+ echo "rendered architecture view → $out ($(ordered_pages | grep -c . ) pages)"
141
+ }
142
+
143
+ # --- 2. Build the Concept Map table (stdout) ---
144
+ build_concept_map() {
145
+ echo "| Concept | Type | Open it when… |"
146
+ echo "|---------|------|---------------|"
147
+ local rel type title desc
148
+ while IFS= read -r -d '' page; do
149
+ rel="${page#"$BUNDLE/"}"
150
+ [[ "$(basename "$rel")" == "index.md" ]] && continue
151
+ type="$(get_yaml_field "$page" type)"
152
+ [[ -n "$type" ]] || continue
153
+ title="$(get_yaml_field "$page" title)"
154
+ [[ -n "$title" ]] || title="$rel"
155
+ # description may be a folded (>) block — take the first non-empty body line.
156
+ desc="$(awk '
157
+ NR==1&&/^---$/{fm=1;next} fm&&/^---$/{exit}
158
+ fm && /^description:/ { collect=1; sub(/^description:[[:space:]]*>?[[:space:]]*/,""); if($0!=""){print; exit} next }
159
+ fm && collect { sub(/^[[:space:]]+/,""); if($0!=""){print; exit} }
160
+ ' "$page")"
161
+ echo "| [${title}](${rel}) | ${type} | ${desc} |"
162
+ done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
163
+ }
164
+
165
+ # Inject the Concept Map between markers in a target file (path may be relative
166
+ # to BUNDLE: links in the map are bundle-relative, so the target should resolve
167
+ # them — wiki/index.md works directly; an index root above wiki/ should prefix).
168
+ inject_concept_map() {
169
+ local target="$1" map="$2"
170
+ [[ -f "$target" ]] || { echo "WARN: concept-map target not found: $target" >&2; return 0; }
171
+ if ! grep -q 'CONCEPT-MAP:START' "$target" || ! grep -q 'CONCEPT-MAP:END' "$target"; then
172
+ echo "WARN: $target has no CONCEPT-MAP markers — skipping injection" >&2
173
+ return 0
174
+ fi
175
+ local tmp; tmp="$(mktemp)"
176
+ awk -v mapfile="$map" '
177
+ /<!-- CONCEPT-MAP:START -->/ { print; while ((getline line < mapfile) > 0) print line; close(mapfile); skip=1; next }
178
+ /<!-- CONCEPT-MAP:END -->/ { skip=0 }
179
+ !skip { print }
180
+ ' "$target" >"$tmp"
181
+ mv "$tmp" "$target"
182
+ echo "injected Concept Map → $target"
183
+ }
184
+
185
+ # --- 3. Render a self-contained offline HTML viewer (single file) ---
186
+ # All pages are inlined as JSON; a small built-in markdown renderer draws them in
187
+ # the browser. No server, no internet, no CDN. jq encodes page content safely
188
+ # (and we neutralize any literal </ so embedded "</script>" can't break parsing).
189
+ render_web() {
190
+ local out="$1"
191
+ command -v jq >/dev/null 2>&1 || { echo "ERROR: --web requires jq" >&2; return 1; }
192
+ local tmp; tmp="$(mktemp)"
193
+
194
+ cat >"$tmp" <<'HTML_HEAD'
195
+ <!doctype html>
196
+ <html lang="en">
197
+ <head>
198
+ <meta charset="utf-8">
199
+ <meta name="viewport" content="width=device-width, initial-scale=1">
200
+ <title>Knowledge Bundle</title>
201
+ <style>
202
+ :root { --bg:#0f1115; --panel:#161a22; --ink:#d7dce5; --muted:#8a93a6; --accent:#6ea8fe; --border:#262c38; --code:#1b2030; }
203
+ * { box-sizing: border-box; }
204
+ body { margin:0; font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; color:var(--ink); background:var(--bg); }
205
+ #app { display:flex; min-height:100vh; }
206
+ #side { width:300px; flex:0 0 300px; background:var(--panel); border-right:1px solid var(--border); height:100vh; overflow:auto; position:sticky; top:0; padding:14px; }
207
+ #side h1 { font-size:14px; margin:0 0 10px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; }
208
+ #search { width:100%; padding:8px 10px; margin-bottom:12px; background:var(--code); border:1px solid var(--border); border-radius:6px; color:var(--ink); }
209
+ .sec { font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:14px 0 4px; }
210
+ .nav a { display:block; padding:4px 8px; color:var(--ink); text-decoration:none; border-radius:5px; font-size:13.5px; }
211
+ .nav a:hover { background:var(--code); }
212
+ .nav a.active { background:var(--accent); color:#0b0e14; }
213
+ .nav a .ty { float:right; font-size:10px; color:var(--muted); }
214
+ .nav a.active .ty { color:#0b0e14; }
215
+ #main { flex:1; max-width:900px; padding:32px 44px; }
216
+ #content h1,#content h2,#content h3 { line-height:1.25; }
217
+ #content h1 { font-size:28px; border-bottom:1px solid var(--border); padding-bottom:8px; }
218
+ #content a { color:var(--accent); }
219
+ #content code { background:var(--code); padding:2px 5px; border-radius:4px; font-size:90%; }
220
+ #content pre { background:var(--code); border:1px solid var(--border); border-radius:8px; padding:12px 14px; overflow:auto; }
221
+ #content pre code { background:none; padding:0; }
222
+ #content pre.mermaid-src { border-left:3px solid var(--accent); }
223
+ #content pre.mermaid-src::before { content:"⬡ Mermaid diagram (source)"; display:block; color:var(--muted); font-size:11px; margin-bottom:6px; }
224
+ #content table { border-collapse:collapse; width:100%; margin:14px 0; font-size:13.5px; }
225
+ #content th,#content td { border:1px solid var(--border); padding:6px 9px; text-align:left; vertical-align:top; }
226
+ #content th { background:var(--code); }
227
+ #content blockquote { border-left:3px solid var(--border); margin:12px 0; padding:2px 14px; color:var(--muted); }
228
+ #content hr { border:none; border-top:1px solid var(--border); margin:22px 0; }
229
+ .crumb { color:var(--muted); font-size:12px; margin-bottom:8px; }
230
+ </style>
231
+ </head>
232
+ <body>
233
+ <div id="app">
234
+ <nav id="side">
235
+ <h1>Knowledge Bundle</h1>
236
+ <input id="search" placeholder="Search…" autocomplete="off">
237
+ <div id="nav" class="nav"></div>
238
+ </nav>
239
+ <main id="main"><div id="content"></div></main>
240
+ </div>
241
+ <script>
242
+ HTML_HEAD
243
+
244
+ # Inline page data: PAGES[rel] = {title, type, md}, plus ORDER (index first).
245
+ {
246
+ echo "const PAGES = {"
247
+ while IFS= read -r -d '' page; do
248
+ local rel title type
249
+ rel="${page#"$BUNDLE/"}"
250
+ title="$(get_yaml_field "$page" title)"; [[ -n "$title" ]] || title="$rel"
251
+ type="$(get_yaml_field "$page" type)"
252
+ printf '%s: {"title": %s, "type": %s, "md": %s},\n' \
253
+ "$(jq -Rn --arg v "$rel" '$v')" \
254
+ "$(jq -Rn --arg v "$title" '$v')" \
255
+ "$(jq -Rn --arg v "$type" '$v')" \
256
+ "$(strip_frontmatter "$page" | jq -Rs . | sed 's#</#<\\/#g')"
257
+ done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
258
+ echo "};"
259
+ # ORDER: bundle root index.md first, then everything else sorted.
260
+ echo "const ORDER = Object.keys(PAGES).sort(function(a,b){"
261
+ echo " if(a==='index.md') return -1; if(b==='index.md') return 1;"
262
+ echo " return a<b?-1:a>b?1:0; });"
263
+ } >>"$tmp"
264
+
265
+ cat >>"$tmp" <<'HTML_TAIL'
266
+ function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
267
+ function resolve(base, href){
268
+ if(/^[a-z]+:\/\//.test(href)||href[0]==='#') return href;
269
+ var dir = base.indexOf('/')<0 ? '' : base.replace(/\/[^/]*$/,'');
270
+ var parts = (dir? dir.split('/'):[]).concat(href.split('/')), out=[];
271
+ for(var i=0;i<parts.length;i++){ var p=parts[i];
272
+ if(p==='..') out.pop(); else if(p!=='.'&&p!=='') out.push(p); }
273
+ return out.join('/');
274
+ }
275
+ function inline(s, base){
276
+ s = s.replace(/`([^`]+)`/g, function(m,c){return '<code>'+esc(c)+'</code>';});
277
+ s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, function(m,t,u){
278
+ if(/^[a-z]+:\/\//.test(u)) return '<a href="'+u+'" target="_blank" rel="noopener">'+t+'</a>';
279
+ var key=resolve(base,u);
280
+ if(PAGES[key]) return '<a href="#'+key+'" data-nav="'+key+'">'+t+'</a>';
281
+ return '<span title="'+esc(u)+'">'+t+'</span>';
282
+ });
283
+ s = s.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>');
284
+ s = s.replace(/(^|[^*])\*([^*\n]+)\*/g,'$1<em>$2</em>');
285
+ return s;
286
+ }
287
+ function render(md, base){
288
+ // Pull fenced code blocks out first so their contents aren't block-parsed.
289
+ var blocks=[], src=md.replace(/```(\w*)\n([\s\S]*?)```/g,function(m,lang,body){
290
+ var cls = lang==='mermaid' ? ' class="mermaid-src"' : '';
291
+ blocks.push('<pre'+cls+'><code>'+esc(body.replace(/\n$/,''))+'</code></pre>');
292
+ return 'BLOCK'+(blocks.length-1)+'';
293
+ });
294
+ var lines=src.split('\n'), out='', i=0, list='', tbl=[];
295
+ function closeList(){ if(list){ out+='</'+list+'>'; list=''; } }
296
+ function flushTbl(){
297
+ if(!tbl.length) return;
298
+ var rows=tbl.filter(function(r){return !/^\s*\|?[\s:|-]+\|?\s*$/.test(r);});
299
+ out+='<table>';
300
+ rows.forEach(function(r,ri){
301
+ var cells=r.replace(/^\||\|$/g,'').split('|');
302
+ out+='<tr>'+cells.map(function(c){var t=ri===0?'th':'td';return '<'+t+'>'+inline(c.trim(),base)+'</'+t+'>';}).join('')+'</tr>';
303
+ });
304
+ out+='</table>'; tbl=[];
305
+ }
306
+ for(;i<lines.length;i++){
307
+ var ln=lines[i];
308
+ if(/^\s*\|.*\|\s*$/.test(ln)){ closeList(); tbl.push(ln); continue; } else flushTbl();
309
+ var h=ln.match(/^(#{1,6})\s+(.*)$/);
310
+ if(h){ closeList(); out+='<h'+h[1].length+'>'+inline(esc(h[2]),base)+'</h'+h[1].length+'>'; continue; }
311
+ if(/^\s*---\s*$/.test(ln)){ closeList(); out+='<hr>'; continue; }
312
+ if(/^\s*>\s?/.test(ln)){ closeList(); out+='<blockquote>'+inline(esc(ln.replace(/^\s*>\s?/,'')),base)+'</blockquote>'; continue; }
313
+ var li=ln.match(/^\s*([-*]|\d+\.)\s+(.*)$/);
314
+ if(li){ var want=/^\d/.test(li[1])?'ol':'ul'; if(list!==want){ closeList(); list=want; out+='<'+want+'>'; } out+='<li>'+inline(esc(li[2]),base)+'</li>'; continue; }
315
+ var b=ln.match(/^BLOCK(\d+)$/);
316
+ if(b){ closeList(); out+=blocks[+b[1]]; continue; }
317
+ if(/^\s*$/.test(ln)){ closeList(); continue; }
318
+ closeList(); out+='<p>'+inline(esc(ln),base)+'</p>';
319
+ }
320
+ flushTbl(); closeList();
321
+ return out;
322
+ }
323
+ var navEl=document.getElementById('nav'), contentEl=document.getElementById('content');
324
+ function section(k){ return k.indexOf('/')<0 ? '(root)' : k.split('/')[0]; }
325
+ function buildNav(filter){
326
+ navEl.innerHTML=''; var lastSec=null;
327
+ ORDER.forEach(function(k){
328
+ var p=PAGES[k];
329
+ if(filter && (p.title+' '+p.md).toLowerCase().indexOf(filter)<0) return;
330
+ var sec=section(k);
331
+ if(sec!==lastSec){ var s=document.createElement('div'); s.className='sec'; s.textContent=sec; navEl.appendChild(s); lastSec=sec; }
332
+ var a=document.createElement('a'); a.href='#'+k; a.dataset.nav=k;
333
+ a.innerHTML=esc(p.title)+(p.type?'<span class="ty">'+esc(p.type)+'</span>':'');
334
+ navEl.appendChild(a);
335
+ });
336
+ }
337
+ function show(k){
338
+ var p=PAGES[k]; if(!p){ k=ORDER[0]; p=PAGES[k]; }
339
+ contentEl.innerHTML='<div class="crumb">'+esc(k)+'</div>'+render(p.md,k);
340
+ document.querySelectorAll('#nav a').forEach(function(a){ a.classList.toggle('active', a.dataset.nav===k); });
341
+ if(location.hash.slice(1)!==k) history.replaceState(null,'','#'+k);
342
+ contentEl.parentElement.scrollTop=0; window.scrollTo(0,0);
343
+ }
344
+ document.addEventListener('click',function(e){ var a=e.target.closest('[data-nav]'); if(a){ e.preventDefault(); show(a.dataset.nav); } });
345
+ document.getElementById('search').addEventListener('input',function(e){ buildNav(e.target.value.toLowerCase().trim()); });
346
+ window.addEventListener('hashchange',function(){ var k=decodeURIComponent(location.hash.slice(1)); if(PAGES[k]) show(k); });
347
+ buildNav('');
348
+ show(decodeURIComponent(location.hash.slice(1)) || ORDER[0]);
349
+ </script>
350
+ </body>
351
+ </html>
352
+ HTML_TAIL
353
+
354
+ mkdir -p "$(dirname "$out")"
355
+ mv "$tmp" "$out"
356
+ echo "rendered offline HTML viewer → $out ($(find "$BUNDLE" -type f -name '*.md' | grep -c .) pages)"
357
+ }
358
+
359
+ [[ -n "$ARCH_OUT" ]] && render_architecture "$ARCH_OUT"
360
+
361
+ if [[ ${#CMAP_INTO[@]} -gt 0 ]]; then
362
+ MAP_TMP="$(mktemp)"
363
+ build_concept_map >"$MAP_TMP"
364
+ for tgt in "${CMAP_INTO[@]}"; do
365
+ inject_concept_map "$tgt" "$MAP_TMP"
366
+ done
367
+ rm -f "$MAP_TMP"
368
+ fi
369
+
370
+ [[ -n "$WEB_OUT" ]] && render_web "$WEB_OUT"
371
+
372
+ [[ -n "$ARCH_OUT" || -n "$WEB_OUT" || ${#CMAP_INTO[@]} -gt 0 ]] || { echo "ERROR: nothing to do (pass --arch-out, --web, and/or --concept-map-into)" >&2; exit 1; }
373
+ exit 0