@drafthq/draft
Advanced tools
| { | ||
| "name": "draft", | ||
| "displayName": "Draft", | ||
| "description": "Context-Driven Development: draft specs and plans before implementation. Structured workflows for features and fixes.", | ||
| "version": "3.3.0", | ||
| "skills": "./skills/", | ||
| "agents": "./core/agents/", | ||
| "author": { | ||
| "name": "mayurpise" | ||
| }, | ||
| "homepage": "https://github.com/drafthq/draft", | ||
| "license": "MIT", | ||
| "keywords": [ | ||
| "context-driven-development", | ||
| "ai-assisted", | ||
| "planning", | ||
| "specifications", | ||
| "architecture", | ||
| "code-review", | ||
| "monorepo", | ||
| "tdd", | ||
| "validation", | ||
| "enterprise", | ||
| "incremental-refresh", | ||
| "signal-detection", | ||
| "state-management" | ||
| ] | ||
| } |
| 'use strict'; | ||
| // Cursor reads plugins from the shared Claude plugin registry under ~/.claude/ | ||
| // on many builds, so a file copy alone never surfaces /draft:* commands. This | ||
| // module merges Draft into the three registry files non-destructively: | ||
| // - ~/.claude/plugins/known_marketplaces.json (marketplace entry) | ||
| // - ~/.claude/plugins/installed_plugins.json (install record) | ||
| // - ~/.claude/settings.json (enabledPlugins flag) | ||
| // Every write preserves all other plugins, hooks, and unknown keys. Reads of a | ||
| // corrupt JSON file fail loud rather than silently clobbering user data. | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const log = require('./log'); | ||
| const MARKETPLACE_KEY = 'draft-plugins'; | ||
| const PLUGIN_KEY = `draft@${MARKETPLACE_KEY}`; // name@<marketplace name> | ||
| function readJson(filePath, fallback) { | ||
| try { | ||
| return JSON.parse(fs.readFileSync(filePath, 'utf8')); | ||
| } catch (err) { | ||
| if (err.code === 'ENOENT') return fallback; | ||
| throw new Error(`Cannot parse ${filePath}: ${err.message}`); | ||
| } | ||
| } | ||
| function writeJsonAtomic(filePath, data) { | ||
| const dir = path.dirname(filePath); | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| const tmp = `${filePath}.tmp.${process.pid}`; | ||
| fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8'); | ||
| fs.renameSync(tmp, filePath); | ||
| } | ||
| function claudeHome(home) { | ||
| return path.join(home, '.claude'); | ||
| } | ||
| function registryPaths(home) { | ||
| const base = path.join(claudeHome(home), 'plugins'); | ||
| return { | ||
| kmPath: path.join(base, 'known_marketplaces.json'), | ||
| ipPath: path.join(base, 'installed_plugins.json'), | ||
| settingsPath: path.join(claudeHome(home), 'settings.json'), | ||
| }; | ||
| } | ||
| function registryScope(scope) { | ||
| return scope === 'project' ? 'project' : 'user'; | ||
| } | ||
| // Pure: compute the merged registry objects without touching disk. Callers that | ||
| // want to persist them use applyCursorRegistration (which delegates here first). | ||
| function registerCursorPlugin(opts) { | ||
| const { home, version, scope } = opts; | ||
| const installPath = path.resolve(opts.installPath); | ||
| const now = new Date().toISOString(); | ||
| const paths = registryPaths(home); | ||
| // --- known_marketplaces.json: overwrite only our key. --- | ||
| const km = readJson(paths.kmPath, {}); | ||
| km[MARKETPLACE_KEY] = { | ||
| source: { source: 'directory', path: installPath }, | ||
| installLocation: installPath, | ||
| lastUpdated: now, | ||
| }; | ||
| // --- installed_plugins.json: merge our key, preserve installedAt on upgrade. --- | ||
| const ip = readJson(paths.ipPath, { version: 2, plugins: {} }); | ||
| if (typeof ip.version !== 'number') ip.version = 2; | ||
| if (!ip.plugins || typeof ip.plugins !== 'object') ip.plugins = {}; | ||
| const existing = Array.isArray(ip.plugins[PLUGIN_KEY]) ? ip.plugins[PLUGIN_KEY][0] : null; | ||
| const installedAt = existing && existing.installedAt ? existing.installedAt : now; | ||
| ip.plugins[PLUGIN_KEY] = [ | ||
| { | ||
| scope: registryScope(scope), | ||
| installPath, | ||
| version, | ||
| installedAt, | ||
| lastUpdated: now, | ||
| }, | ||
| ]; | ||
| // --- settings.json: flip our enabledPlugins flag, preserve everything else. --- | ||
| const settings = readJson(paths.settingsPath, {}); | ||
| if (!settings.enabledPlugins || typeof settings.enabledPlugins !== 'object') { | ||
| settings.enabledPlugins = {}; | ||
| } | ||
| settings.enabledPlugins[PLUGIN_KEY] = true; | ||
| return { | ||
| kmPath: paths.kmPath, | ||
| km, | ||
| ipPath: paths.ipPath, | ||
| ip, | ||
| settingsPath: paths.settingsPath, | ||
| settings, | ||
| }; | ||
| } | ||
| // Compute the merges and, unless dryRun, persist them atomically. Returns the | ||
| // same shape as registerCursorPlugin so the installer can log the paths. | ||
| function applyCursorRegistration(opts) { | ||
| const result = registerCursorPlugin(opts); | ||
| if (opts.dryRun) return result; | ||
| log.plan(`writing: ${result.kmPath}`); | ||
| writeJsonAtomic(result.kmPath, result.km); | ||
| log.plan(`writing: ${result.ipPath}`); | ||
| writeJsonAtomic(result.ipPath, result.ip); | ||
| log.plan(`writing: ${result.settingsPath}`); | ||
| writeJsonAtomic(result.settingsPath, result.settings); | ||
| return result; | ||
| } | ||
| module.exports = { | ||
| PLUGIN_KEY, | ||
| MARKETPLACE_KEY, | ||
| registerCursorPlugin, | ||
| applyCursorRegistration, | ||
| }; |
| 'use strict'; | ||
| // Read name/version from a plugin manifest JSON (.cursor-plugin/plugin.json or | ||
| // .claude-plugin/plugin.json). Fails loud if either required field is missing — | ||
| // a manifest without a version would corrupt the registry's install record. | ||
| const fs = require('fs'); | ||
| function readPluginManifest(manifestPath) { | ||
| const raw = fs.readFileSync(manifestPath, 'utf8'); | ||
| const data = JSON.parse(raw); | ||
| if (!data.name) throw new Error(`Missing name in ${manifestPath}`); | ||
| if (!data.version) throw new Error(`Missing version in ${manifestPath}`); | ||
| return data; | ||
| } | ||
| function readPluginVersion(manifestPath) { | ||
| return readPluginManifest(manifestPath).version; | ||
| } | ||
| module.exports = { readPluginManifest, readPluginVersion }; |
| --- | ||
| project: "{PROJECT_NAME}" | ||
| module: "{MODULE_NAME or 'root'}" | ||
| generated_by: "draft:init" | ||
| generated_at: "{ISO_TIMESTAMP}" | ||
| draft_init_mode: okf | ||
| --- | ||
| # {PROJECT_NAME} — AI Context Index | ||
| > Index root for the OKF taxonomy bundle (`wiki/`). Read **Synopsis** for broad | ||
| > tasks (they usually terminate here). For focused tasks, route through the | ||
| > **Concept Map** to ≤N concept pages — each lists `x-grounded-paths`. This is | ||
| > both the cheap broad-context path AND the progressive-disclosure entry point. | ||
| ## Synopsis | ||
| <!-- 150–250 lines: the cheap broad-context path (prior .ai-context.md value | ||
| preserved). Architecture in brief, key invariants, where to start, top | ||
| hotspots. A broad task should be answerable from this section alone. --> | ||
| - **Architecture in brief:** {2–4 sentences} | ||
| - **Key invariants:** {bullet list, provenance-tagged} | ||
| - **Where to start:** {entrypoints + core subsystems} | ||
| - **Top hotspots:** {from hotspot-rank.sh — symbol, fan-in} | ||
| ## Concept Map | ||
| <!-- Routing table built from each concept's frontmatter `description`. | ||
| Open a section index for the full per-concept list. --> | ||
| | Section | Routing | | ||
| |---------|---------| | ||
| | `wiki/systems/` | {one-line per subsystem — what it owns, when to open} | | ||
| | `wiki/features/` | {one-line per feature} | | ||
| | `wiki/reference/` | config, schemas, APIs, ADRs, runbooks | | ||
| | `wiki/entrypoints/` | binaries / CLIs / handler roots | | ||
| Full taxonomy: [wiki/index.md](wiki/index.md). | ||
| ## How to navigate | ||
| 1. **Broad task** (summarize, "what owns X", topology) → answer from **Synopsis**. | ||
| 2. **Focused task** ("what breaks if I change Y", "add a field to Z") → open the | ||
| matching concept via the **Concept Map**; follow its `x-grounded-paths` and | ||
| `Used by` cross-links. Do not read the whole bundle. | ||
| 3. Every concept page is verified against the live call graph; trust its | ||
| `Blast radius` section over re-deriving by hand. |
| --- | ||
| type: Subsystem # required (OKF) — one of the frozen vocab below | ||
| title: "{CONCEPT_TITLE}" # OKF | ||
| description: > # OKF — LOAD-BEARING: the agent's routing key. | ||
| Write this as a ROUTING DECISION, not a summary. It must answer | ||
| "should the agent open this file for the task at hand?" from the index | ||
| alone. Name the responsibilities and the words a task would use. | ||
| resource: "{CANONICAL_SOURCE_PATH}" # OKF — canonical source path(s) | ||
| tags: [tag1, tag2] # OKF | ||
| timestamp: "{ISO_TIMESTAMP}" # OKF — last regeneration | ||
| # Draft extensions (ignored by generic OKF consumers; namespaced x-): | ||
| x-grounded-paths: ["{path/a}", "{path/b}"] # exact source files this page grounds | ||
| x-hotspot-score: 0.0 # from hotspot-rank.sh (0..1) | ||
| x-callers: ["{module/a}", "{module/b}"] # from graph-callers.sh | ||
| --- | ||
| # {CONCEPT_TITLE} | ||
| <!-- | ||
| Frozen `type` vocabulary (changing it churns every file — versioned via | ||
| index.md: okf_types_version): | ||
| Subsystem — major graph cluster / package boundary → systems/ | ||
| Module — single package/dir with cohesive responsibility → systems/ | ||
| Feature — user-facing capability spanning modules → features/ | ||
| Entrypoint — binary / main / CLI / handler root → entrypoints/ | ||
| API — public interface, route group, RPC surface → reference/ | ||
| DataModel — schema, table, core struct/type → reference/ | ||
| Dependency — notable external dep + how it's used → reference/ | ||
| ADR — architecture decision record → reference/ | ||
| Runbook — operational procedure → reference/ | ||
| --> | ||
| ## What it is | ||
| One paragraph: the concept's responsibility and boundary. Graph-grounded. | ||
| ## How it works | ||
| Primary control/data flow. At least one Mermaid diagram for a significant | ||
| concept (workflow, state, or sequence). Grounded in the call graph. | ||
| ## Used by | ||
| Cross-links to callers (from `x-callers`). Each link is a relative path to | ||
| another concept page so `okf-validate.sh` can resolve it. | ||
| ## Blast radius | ||
| What breaks if this changes (from `graph-impact.sh`). Lists `x-grounded-paths` | ||
| so a focused task knows exactly which source files to open. | ||
| ## See also | ||
| - [Related concept](../systems/other.md) |
| --- | ||
| type: Subsystem | ||
| title: "{PROJECT_NAME} — Knowledge Bundle" | ||
| description: > | ||
| Root index of the OKF taxonomy bundle. Start here, then route into | ||
| overview/, systems/, features/, reference/, or entrypoints/ via the | ||
| Concept Map. Open a concept only when its description matches the task. | ||
| resource: . | ||
| tags: [index] | ||
| timestamp: "{ISO_TIMESTAMP}" | ||
| okf_version: "0.1" | ||
| okf_types_version: "0.1" | ||
| --- | ||
| # {PROJECT_NAME} — Knowledge Bundle | ||
| > OKF v0.1 bundle. One concept per file; cross-links form the graph. The | ||
| > live call graph (`codebase-memory-mcp`) is the grounding source; this bundle | ||
| > is the navigable serialization. `../ai-context.md` is the consumption entry point. | ||
| ## Sections | ||
| | Section | Holds | Index | | ||
| |---------|-------|-------| | ||
| | `overview/` | System map, getting-started, glossary | [overview/index.md](overview/index.md) | | ||
| | `systems/` | Subsystems & modules (graph clusters) | [systems/index.md](systems/index.md) | | ||
| | `features/` | User-facing capabilities spanning modules | [features/index.md](features/index.md) | | ||
| | `reference/` | APIs, data models, dependencies, ADRs, runbooks | [reference/index.md](reference/index.md) | | ||
| | `entrypoints/` | Binaries / mains / CLIs / handler roots | see pages below | | ||
| ## Concept Map | ||
| <!-- Built from each concept's frontmatter `description` (the routing key). | ||
| One line per concept. Regenerated on every init/refresh. --> | ||
| <!-- CONCEPT-MAP:START --> | ||
| <!-- CONCEPT-MAP:END --> | ||
| ## Change log | ||
| See [log.md](log.md) for chronological regeneration history. |
| --- | ||
| type: Subsystem | ||
| title: "{SECTION_TITLE}" | ||
| description: > | ||
| Section index. Lists every concept in this section with its one-line | ||
| routing description so an agent can pick the right page without opening | ||
| each one. {SECTION_PURPOSE} | ||
| resource: . | ||
| tags: [index] | ||
| timestamp: "{ISO_TIMESTAMP}" | ||
| --- | ||
| # {SECTION_TITLE} | ||
| > Section of the OKF bundle. Back to [bundle root](../index.md). | ||
| ## Concepts | ||
| <!-- One row per concept page in this section. `description` is the routing key | ||
| copied from each page's frontmatter. Regenerated on every init/refresh. --> | ||
| | Concept | Type | Routing description | | ||
| |---------|------|---------------------| | ||
| | [{concept-a}]({concept-a}.md) | Module | {one-line routing desc} | | ||
| | [{concept-b}]({concept-b}.md) | Feature | {one-line routing desc} | |
| #!/usr/bin/env bash | ||
| # graph-preflight.sh — read-only go/no-go check before indexing a repo with the | ||
| # Draft knowledge-graph engine (codebase-memory-mcp). | ||
| # | ||
| # Indexes NOTHING. Walks git metadata + engine status only. Safe to run anywhere. | ||
| # Companion preflight for `scripts/tools/graph-init.sh` / `/draft:init --graph-only`. | ||
| # | ||
| # Usage: scripts/tools/graph-preflight.sh [--json] [REPO_PATH] (default repo: cwd) | ||
| # --json emit a machine-readable report on stdout (no human output) | ||
| # Exit: 0 = GO / GO-with-caution, 1 = NO-GO (blocking), 2 = bad invocation. | ||
| # | ||
| # Deliberately uses guard idioms (`|| true`, `|| echo 0`) rather than aborting: | ||
| # the report accumulates blockers/warnings and prints a verdict even under -e. | ||
| set -euo pipefail | ||
| TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| SELF_REPO="$(cd "$TOOLS_DIR/../.." && pwd)" | ||
| # shellcheck source=_lib.sh | ||
| source "$TOOLS_DIR/_lib.sh" | ||
| usage() { | ||
| cat <<'EOF' | ||
| graph-preflight.sh — read-only go/no-go check before knowledge-graph indexing. | ||
| Usage: | ||
| scripts/tools/graph-preflight.sh [--json] [REPO_PATH] (default repo: cwd) | ||
| Flags: | ||
| --json Emit a machine-readable report on stdout (no human output). | ||
| --help Show this help. | ||
| Indexes nothing — walks git metadata + engine status only. | ||
| Exit codes: 0 GO / GO-with-caution, 1 NO-GO (blocking), 2 bad invocation. | ||
| EOF | ||
| } | ||
| # --- args --- | ||
| JSON_MODE=0 | ||
| REPO="" | ||
| while [[ $# -gt 0 ]]; do | ||
| case "$1" in | ||
| --json) JSON_MODE=1; shift;; | ||
| -h|--help) usage; exit 0;; | ||
| -*) echo "Unknown flag: $1" >&2; usage >&2; exit 2;; | ||
| *) if [[ -z "$REPO" ]]; then REPO="$1"; else echo "Unexpected arg: $1" >&2; exit 2; fi; shift;; | ||
| esac | ||
| done | ||
| REPO="${REPO:-.}" | ||
| [[ -d "$REPO" ]] || { echo "ERROR: '$REPO' is not a directory" >&2; exit 2; } | ||
| REPO_ABS="$(cd "$REPO" && pwd)" | ||
| # --- formatting (color only on a tty, and never in --json) --- | ||
| 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 | ||
| hr() { [[ "$JSON_MODE" -eq 0 ]] && printf '%s\n' "------------------------------------------------------------"; return 0; } | ||
| sec() { [[ "$JSON_MODE" -eq 0 ]] && { echo; printf '%s== %s ==%s\n' "$B" "$1" "$D"; }; return 0; } | ||
| ok() { [[ "$JSON_MODE" -eq 0 ]] && printf ' %s[ OK ]%s %s\n' "$G" "$D" "$1"; return 0; } | ||
| info() { [[ "$JSON_MODE" -eq 0 ]] && printf ' %s\n' "$1"; return 0; } | ||
| 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; } | ||
| 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; } | ||
| WARNINGS=0; BLOCKERS=0; WARN_J=""; FAIL_J="" | ||
| # --- helpers --- | ||
| count_files() { { git -C "$REPO_ABS" ls-files -- "$@" 2>/dev/null || true; } | wc -l | tr -d ' '; } | ||
| count_loc() { | ||
| [[ -n "$(git -C "$REPO_ABS" ls-files -- "$@" 2>/dev/null | head -1)" ]] || { echo 0; return; } | ||
| # cat must run from the repo root so tracked paths resolve. | ||
| ( cd "$REPO_ABS" && git ls-files -z -- "$@" 2>/dev/null | xargs -0 cat 2>/dev/null ) | wc -l | tr -d ' ' || true | ||
| } | ||
| human() { awk -v n="$1" 'BEGIN{ v=n; split("K M B",u); if(v<1000){printf "%d",v;exit} | ||
| for(i=1;i<=3;i++){v/=1000; if(v<1000){printf "%.1f%s",v,u[i];exit}}}'; } | ||
| # --- collected fields (defaults so --json is always well-formed) --- | ||
| IS_GIT=false; AT_ROOT=false; GIT_TOP=""; COMMIT="none" | ||
| TRACKED=0; ALLDISK=0; TOTAL_LOC=0; CCGO_LOC=0; LANG_J="" | ||
| VEND_J=""; ENGINE=""; VER=""; LIMIT=""; ENGINE_FOUND=false | ||
| RAM_GB=""; FREE_GB="" | ||
| hr | ||
| [[ "$JSON_MODE" -eq 0 ]] && printf '%sDraft graph pre-flight%s — %s\n' "$B" "$D" "$REPO_ABS" || true | ||
| hr | ||
| # ============================================================ | ||
| sec "1. Git boundary" | ||
| # ============================================================ | ||
| GIT_TOP="$(git -C "$REPO_ABS" rev-parse --show-toplevel 2>/dev/null || true)" | ||
| if [[ -z "$GIT_TOP" ]]; then | ||
| fail "Not inside a git repo. The engine would raw-walk the filesystem (no .gitignore filter)." | ||
| info "Point this at a real git repo root, never a parent container dir." | ||
| GIT_OK=0 | ||
| else | ||
| GIT_OK=1; IS_GIT=true | ||
| if [[ "$GIT_TOP" != "$REPO_ABS" ]]; then | ||
| warn "Not at the git root. Git top is: $GIT_TOP" | ||
| info "Run /draft:init --graph-only at the git root for whole-repo coverage." | ||
| else | ||
| AT_ROOT=true | ||
| ok "Git root: $GIT_TOP" | ||
| fi | ||
| COMMIT="$(git -C "$REPO_ABS" rev-parse --short HEAD 2>/dev/null || echo none)" | ||
| info "HEAD: $COMMIT" | ||
| [[ -f "$GIT_TOP/.gitmodules" ]] && warn "Submodules present — engine indexes the superproject's tracked tree; submodule contents may need separate indexing." || true | ||
| fi | ||
| # ============================================================ | ||
| sec "2. Index scope (git-tracked = what actually gets indexed)" | ||
| # ============================================================ | ||
| if [[ "$GIT_OK" -eq 1 ]]; then | ||
| TRACKED="$(count_files)" | ||
| ALLDISK="$({ find "$REPO_ABS" -type d -name .git -prune -o -type f -print 2>/dev/null || true; } | wc -l | tr -d ' ')" | ||
| ok "Git-tracked files: $TRACKED (on disk: $ALLDISK — the difference is gitignored and SKIPPED)" | ||
| [[ "$JSON_MODE" -eq 0 ]] && { echo; printf ' %-14s %10s %12s\n' "language" "files" "lines"; printf ' %-14s %10s %12s\n' "--------" "-----" "-----"; } || true | ||
| declare -A GLOBS=( | ||
| [C/C++]='*.c *.cc *.cpp *.cxx *.h *.hpp *.hh *.hxx' | ||
| [Go]='*.go' | ||
| [Python]='*.py' | ||
| [TS/JS]='*.ts *.tsx *.js *.jsx *.mjs' | ||
| [Rust]='*.rs' | ||
| [Java]='*.java' | ||
| ) | ||
| for lang in "C/C++" Go Python TS/JS Rust Java; do | ||
| # shellcheck disable=SC2086 | ||
| read -ra g <<< "${GLOBS[$lang]}" | ||
| f="$(count_files "${g[@]}")" | ||
| [[ "$f" -eq 0 ]] && continue | ||
| l="$(count_loc "${g[@]}")" | ||
| [[ "$JSON_MODE" -eq 0 ]] && printf ' %-14s %10s %12s\n' "$lang" "$f" "$l" || true | ||
| LANG_J="${LANG_J:+$LANG_J,}{\"lang\":\"$(json_escape "$lang")\",\"files\":$f,\"lines\":$l}" | ||
| TOTAL_LOC=$((TOTAL_LOC + l)) | ||
| [[ "$lang" == "C/C++" || "$lang" == "Go" ]] && CCGO_LOC=$((CCGO_LOC + l)) || true | ||
| done | ||
| [[ "$JSON_MODE" -eq 0 ]] && printf ' %-14s %10s %12s\n' "TOTAL" "$TRACKED" "$TOTAL_LOC" || true | ||
| info "Source LOC total: $(human "$TOTAL_LOC") | C/C++/Go: $(human "$CCGO_LOC")" | ||
| else | ||
| warn "Skipped — no git repo." | ||
| fi | ||
| # ============================================================ | ||
| sec "3. Committed vendor/generated trees (these WILL be indexed)" | ||
| # ============================================================ | ||
| if [[ "$GIT_OK" -eq 1 ]]; then | ||
| # Match vendor/generated *directories* (token followed by /) and protobuf-generated | ||
| # file suffixes — not filenames that merely contain "gen"/"generate". | ||
| VEND="$(git -C "$REPO_ABS" ls-files 2>/dev/null \ | ||
| | grep -iE '(^|/)(third_party|thirdparty|vendor|external|deps|generated)/|\.pb\.(cc|h|go)$|_pb2\.py$' \ | ||
| | sed -E 's#(^.*/(third_party|thirdparty|vendor|external|deps|generated))/.*#\1/#' \ | ||
| | sort -u | head -40 || true)" | ||
| if [[ -n "$VEND" ]]; then | ||
| warn "Committed vendor/generated paths found — gitignore to exclude, or accept index inflation:" | ||
| while IFS= read -r p; do | ||
| [[ -z "$p" ]] && continue | ||
| info "$p" | ||
| VEND_J="${VEND_J:+$VEND_J,}\"$(json_escape "$p")\"" | ||
| done <<< "$VEND" | ||
| else | ||
| ok "No obvious committed vendor/generated trees." | ||
| fi | ||
| else | ||
| warn "Skipped — no git repo." | ||
| fi | ||
| # ============================================================ | ||
| sec "4. Engine availability" | ||
| # ============================================================ | ||
| if find_memory_bin "$REPO_ABS" "$SELF_REPO"; then | ||
| ENGINE="$MEMORY_BIN" | ||
| ENGINE_FOUND=true | ||
| VER="$("$ENGINE" --version 2>/dev/null | head -1 || echo '?')" | ||
| ok "Engine: $ENGINE ($VER)" | ||
| LIMIT="$("$ENGINE" config list 2>/dev/null | awk '/auto_index_limit/{print $3}' || true)" | ||
| [[ -n "$LIMIT" ]] && info "auto_index_limit: $LIMIT (governs AUTO-index only; explicit index_repository should bypass)" || true | ||
| if [[ "$GIT_OK" -eq 1 && -n "${LIMIT:-}" && "$TRACKED" -gt "$LIMIT" ]]; then | ||
| warn "Tracked files ($TRACKED) > auto_index_limit ($LIMIT) — confirm the explicit index isn't truncated near $LIMIT." | ||
| fi | ||
| else | ||
| ENGINE="" | ||
| fail "Engine 'codebase-memory-mcp' not found (checked \$DRAFT_MEMORY_BIN, PATH, ~/.cache/draft/bin/, vendored bin/<arch>/)." | ||
| info "Install: scripts/fetch-memory-engine.sh (or put the binary on PATH)" | ||
| fi | ||
| # ============================================================ | ||
| sec "5. Machine headroom" | ||
| # ============================================================ | ||
| if [[ -r /proc/meminfo ]]; then | ||
| RAM_GB="$(awk '/MemTotal/{printf "%d", $2/1024/1024}' /proc/meminfo)" | ||
| ok "Total RAM: ${RAM_GB} GB (engine self-budgets ~half)" | ||
| elif command -v sysctl >/dev/null 2>&1; then | ||
| RAM_GB="$(( $(sysctl -n hw.memsize 2>/dev/null || echo 0) / 1024 / 1024 / 1024 ))" | ||
| ok "Total RAM: ${RAM_GB} GB" | ||
| else | ||
| warn "Could not read total RAM." | ||
| fi | ||
| CACHE_DIR="$HOME/.cache"; mkdir -p "$CACHE_DIR" 2>/dev/null || true | ||
| FREE_K="$(df -Pk "$CACHE_DIR" 2>/dev/null | awk 'NR==2{print $4}' || true)" | ||
| if [[ -n "${FREE_K:-}" ]]; then | ||
| FREE_GB=$((FREE_K / 1024 / 1024)) | ||
| 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 | ||
| fi | ||
| # ============================================================ | ||
| # Scale heuristic for first-pass time expectation. | ||
| if [[ "$CCGO_LOC" -ge 5000000 || "$TOTAL_LOC" -ge 5000000 ]]; then | ||
| warn "Large codebase ($(human "$TOTAL_LOC") LOC) — expect a long first-pass index (likely hours). Run backgrounded; incremental thereafter." | ||
| fi | ||
| # --- verdict --- | ||
| if [[ "$BLOCKERS" -gt 0 ]]; then VERDICT="NO_GO"; VEXIT=1 | ||
| elif [[ "$WARNINGS" -gt 0 ]]; then VERDICT="GO_WITH_CAUTION"; VEXIT=0 | ||
| else VERDICT="GO"; VEXIT=0 | ||
| fi | ||
| # ============================================================ | ||
| # Output | ||
| # ============================================================ | ||
| if [[ "$JSON_MODE" -eq 1 ]]; then | ||
| printf '{\n' | ||
| printf ' "repo": "%s",\n' "$(json_escape "$REPO_ABS")" | ||
| printf ' "is_git_repo": %s,\n' "$IS_GIT" | ||
| printf ' "git_root": %s,\n' "$([[ -n "$GIT_TOP" ]] && printf '"%s"' "$(json_escape "$GIT_TOP")" || printf 'null')" | ||
| printf ' "at_git_root": %s,\n' "$AT_ROOT" | ||
| printf ' "head": "%s",\n' "$(json_escape "$COMMIT")" | ||
| printf ' "tracked_files": %s,\n' "$TRACKED" | ||
| printf ' "files_on_disk": %s,\n' "$ALLDISK" | ||
| printf ' "languages": [%s],\n' "$LANG_J" | ||
| printf ' "total_source_loc": %s,\n' "$TOTAL_LOC" | ||
| printf ' "ccgo_loc": %s,\n' "$CCGO_LOC" | ||
| printf ' "committed_vendor_paths": [%s],\n' "$VEND_J" | ||
| printf ' "engine": {"found": %s, "path": %s, "version": %s, "auto_index_limit": %s},\n' \ | ||
| "$ENGINE_FOUND" \ | ||
| "$([[ -n "$ENGINE" ]] && printf '"%s"' "$(json_escape "$ENGINE")" || printf 'null')" \ | ||
| "$([[ -n "$VER" ]] && printf '"%s"' "$(json_escape "$VER")" || printf 'null')" \ | ||
| "${LIMIT:-null}" | ||
| printf ' "machine": {"ram_gb": %s, "cache_free_gb": %s},\n' "${RAM_GB:-null}" "${FREE_GB:-null}" | ||
| printf ' "warnings": [%s],\n' "$WARN_J" | ||
| printf ' "blockers": [%s],\n' "$FAIL_J" | ||
| printf ' "verdict": "%s",\n' "$VERDICT" | ||
| printf ' "exit_code": %s\n' "$VEXIT" | ||
| printf '}\n' | ||
| exit "$VEXIT" | ||
| fi | ||
| sec "Verdict" | ||
| echo | ||
| case "$VERDICT" in | ||
| NO_GO) printf '%s NO-GO %s — %d blocker(s), %d warning(s). Resolve blockers above first.\n' "$R" "$D" "$BLOCKERS" "$WARNINGS";; | ||
| GO_WITH_CAUTION) printf '%s GO (with caution) %s — %d warning(s). Review them, then proceed.\n' "$Y" "$D" "$WARNINGS";; | ||
| GO) printf '%s GO %s — clear to index.\n' "$G" "$D";; | ||
| esac | ||
| cat <<EOF | ||
| Next step (when ready, from the git root): | ||
| scripts/tools/graph-init.sh --scope . --json & # or: /draft:init --graph-only | ||
| ${ENGINE:-codebase-memory-mcp} cli list_projects '{}' | ||
| ${ENGINE:-codebase-memory-mcp} cli index_status '{"project":"<name>"}' | ||
| EOF | ||
| hr | ||
| exit "$VEXIT" |
| #!/usr/bin/env bash | ||
| # okf-render-views.sh — render the demoted views from an OKF taxonomy bundle. | ||
| # | ||
| # The wiki/ bundle is the source of truth. This produces the two derived, | ||
| # human-facing views deterministically (so they never drift from the bundle and | ||
| # carry zero extra maintenance): | ||
| # 1. architecture.md — a single linear concatenation of every concept page, | ||
| # frontmatter stripped, in canonical section order, with a banner + TOC. | ||
| # This is the onboarding "read one doc" view (demoted, not deleted). | ||
| # 2. Concept Map — a routing table injected between the | ||
| # <!-- CONCEPT-MAP:START --> / <!-- CONCEPT-MAP:END --> markers in | ||
| # wiki/index.md (and optionally another index-root file). | ||
| # | ||
| # Usage: | ||
| # okf-render-views.sh <BUNDLE_DIR> --arch-out <FILE> [--concept-map-into <FILE>] | ||
| # | ||
| # BUNDLE_DIR is the wiki/ directory. Exit 0 ok, 1 error, 2 bundle not found. | ||
| set -euo pipefail | ||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| # shellcheck source=scripts/tools/_lib.sh | ||
| source "$SCRIPT_DIR/_lib.sh" | ||
| BUNDLE="" | ||
| ARCH_OUT="" | ||
| WEB_OUT="" | ||
| CMAP_INTO=() | ||
| usage() { | ||
| cat <<'EOF' | ||
| okf-render-views.sh — render architecture.md + Concept Map + HTML viewer from an OKF bundle. | ||
| Usage: | ||
| okf-render-views.sh <BUNDLE_DIR> [--arch-out FILE] [--concept-map-into FILE]... [--web FILE] | ||
| Flags: | ||
| --arch-out FILE Write the rendered linear architecture.md here. | ||
| --concept-map-into FILE Inject the Concept Map between the CONCEPT-MAP markers | ||
| in FILE (repeatable: e.g. wiki/index.md and ai-context.md). | ||
| --web FILE Write a self-contained, offline HTML viewer (single file: | ||
| all pages inlined, built-in markdown renderer, sidebar + | ||
| search). Double-click to open — no server, no internet. | ||
| --help Show this help. | ||
| Requires jq (already a Draft prereq) for --web. Exit 0 ok, 1 error, 2 bundle not found. | ||
| EOF | ||
| } | ||
| while [[ $# -gt 0 ]]; do | ||
| case "$1" in | ||
| --arch-out) ARCH_OUT="$2"; shift 2;; | ||
| --concept-map-into) CMAP_INTO+=("$2"); shift 2;; | ||
| --web) WEB_OUT="$2"; shift 2;; | ||
| --help|-h) usage; exit 0;; | ||
| -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;; | ||
| *) | ||
| if [[ -z "$BUNDLE" ]]; then BUNDLE="$1"; else echo "Unexpected arg: $1" >&2; exit 1; fi | ||
| shift | ||
| ;; | ||
| esac | ||
| done | ||
| [[ -n "$BUNDLE" ]] || { usage >&2; exit 1; } | ||
| [[ -d "$BUNDLE" ]] || { echo "ERROR: bundle directory not found: $BUNDLE" >&2; exit 2; } | ||
| BUNDLE="${BUNDLE%/}" | ||
| # Canonical section order for the linear render. Sections not present are skipped. | ||
| SECTIONS=(overview systems features reference entrypoints) | ||
| # Emit bundle-relative page paths in canonical order: for each section, its | ||
| # index.md first, then the rest alphabetically. Pages outside these sections | ||
| # (e.g. log.md, the bundle root index.md) are excluded from the linear view. | ||
| ordered_pages() { | ||
| local sec dir f | ||
| for sec in "${SECTIONS[@]}"; do | ||
| dir="$BUNDLE/$sec" | ||
| [[ -d "$dir" ]] || continue | ||
| [[ -f "$dir/index.md" ]] && echo "$sec/index.md" | ||
| while IFS= read -r f; do | ||
| [[ "$(basename "$f")" == "index.md" ]] && continue | ||
| echo "$sec/${f##*/}" | ||
| done < <(find "$dir" -maxdepth 1 -type f -name '*.md' | sort) | ||
| done | ||
| } | ||
| # Strip YAML frontmatter from a page (leading --- ... --- block on line 1). | ||
| strip_frontmatter() { | ||
| awk ' | ||
| NR==1 && /^---$/ { fm=1; next } | ||
| fm && /^---$/ { fm=0; next } | ||
| !fm { print } | ||
| ' "$1" | ||
| } | ||
| # --- 1. Render architecture.md --- | ||
| render_architecture() { | ||
| local out="$1" | ||
| local tmp; tmp="$(mktemp)" | ||
| { | ||
| echo "---" | ||
| echo "generated_by: \"draft:init (okf-render-views.sh)\"" | ||
| echo "view: rendered" | ||
| echo "source_of_truth: \"wiki/\"" | ||
| echo "---" | ||
| echo "" | ||
| echo "# Architecture (Rendered View)" | ||
| echo "" | ||
| echo "> **Generated** from the \`wiki/\` OKF bundle — do not edit by hand." | ||
| echo "> The bundle is the source of truth; this is the single-document linear" | ||
| echo "> view for onboarding. Regenerate with \`okf-render-views.sh\`." | ||
| echo "" | ||
| echo "## Contents" | ||
| echo "" | ||
| # TOC from page titles. | ||
| local rel title sec last_sec="" | ||
| while IFS= read -r rel; do | ||
| [[ -z "$rel" ]] && continue | ||
| sec="${rel%%/*}" | ||
| if [[ "$sec" != "$last_sec" ]]; then | ||
| echo "- **${sec}/**" | ||
| last_sec="$sec" | ||
| fi | ||
| title="$(get_yaml_field "$BUNDLE/$rel" title)" | ||
| [[ -n "$title" ]] || title="$rel" | ||
| local anchor; anchor="$(printf '%s' "$title" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-')" | ||
| anchor="${anchor#-}"; anchor="${anchor%-}" | ||
| echo " - [${title}](#${anchor})" | ||
| done < <(ordered_pages) | ||
| echo "" | ||
| # Body: each page, frontmatter stripped. | ||
| while IFS= read -r rel; do | ||
| [[ -z "$rel" ]] && continue | ||
| echo "" | ||
| echo "---" | ||
| echo "" | ||
| strip_frontmatter "$BUNDLE/$rel" | ||
| done < <(ordered_pages) | ||
| } >"$tmp" | ||
| mv "$tmp" "$out" | ||
| echo "rendered architecture view → $out ($(ordered_pages | grep -c . ) pages)" | ||
| } | ||
| # --- 2. Build the Concept Map table (stdout) --- | ||
| build_concept_map() { | ||
| echo "| Concept | Type | Open it when… |" | ||
| echo "|---------|------|---------------|" | ||
| local rel type title desc | ||
| while IFS= read -r -d '' page; do | ||
| rel="${page#"$BUNDLE/"}" | ||
| [[ "$(basename "$rel")" == "index.md" ]] && continue | ||
| type="$(get_yaml_field "$page" type)" | ||
| [[ -n "$type" ]] || continue | ||
| title="$(get_yaml_field "$page" title)" | ||
| [[ -n "$title" ]] || title="$rel" | ||
| # description may be a folded (>) block — take the first non-empty body line. | ||
| desc="$(awk ' | ||
| NR==1&&/^---$/{fm=1;next} fm&&/^---$/{exit} | ||
| fm && /^description:/ { collect=1; sub(/^description:[[:space:]]*>?[[:space:]]*/,""); if($0!=""){print; exit} next } | ||
| fm && collect { sub(/^[[:space:]]+/,""); if($0!=""){print; exit} } | ||
| ' "$page")" | ||
| echo "| [${title}](${rel}) | ${type} | ${desc} |" | ||
| done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z) | ||
| } | ||
| # Inject the Concept Map between markers in a target file (path may be relative | ||
| # to BUNDLE: links in the map are bundle-relative, so the target should resolve | ||
| # them — wiki/index.md works directly; an index root above wiki/ should prefix). | ||
| inject_concept_map() { | ||
| local target="$1" map="$2" | ||
| [[ -f "$target" ]] || { echo "WARN: concept-map target not found: $target" >&2; return 0; } | ||
| if ! grep -q 'CONCEPT-MAP:START' "$target" || ! grep -q 'CONCEPT-MAP:END' "$target"; then | ||
| echo "WARN: $target has no CONCEPT-MAP markers — skipping injection" >&2 | ||
| return 0 | ||
| fi | ||
| local tmp; tmp="$(mktemp)" | ||
| awk -v mapfile="$map" ' | ||
| /<!-- CONCEPT-MAP:START -->/ { print; while ((getline line < mapfile) > 0) print line; close(mapfile); skip=1; next } | ||
| /<!-- CONCEPT-MAP:END -->/ { skip=0 } | ||
| !skip { print } | ||
| ' "$target" >"$tmp" | ||
| mv "$tmp" "$target" | ||
| echo "injected Concept Map → $target" | ||
| } | ||
| # --- 3. Render a self-contained offline HTML viewer (single file) --- | ||
| # All pages are inlined as JSON; a small built-in markdown renderer draws them in | ||
| # the browser. No server, no internet, no CDN. jq encodes page content safely | ||
| # (and we neutralize any literal </ so embedded "</script>" can't break parsing). | ||
| render_web() { | ||
| local out="$1" | ||
| command -v jq >/dev/null 2>&1 || { echo "ERROR: --web requires jq" >&2; return 1; } | ||
| local tmp; tmp="$(mktemp)" | ||
| cat >"$tmp" <<'HTML_HEAD' | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| <title>Knowledge Bundle</title> | ||
| <style> | ||
| :root { --bg:#0f1115; --panel:#161a22; --ink:#d7dce5; --muted:#8a93a6; --accent:#6ea8fe; --border:#262c38; --code:#1b2030; } | ||
| * { box-sizing: border-box; } | ||
| body { margin:0; font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; color:var(--ink); background:var(--bg); } | ||
| #app { display:flex; min-height:100vh; } | ||
| #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; } | ||
| #side h1 { font-size:14px; margin:0 0 10px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; } | ||
| #search { width:100%; padding:8px 10px; margin-bottom:12px; background:var(--code); border:1px solid var(--border); border-radius:6px; color:var(--ink); } | ||
| .sec { font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:14px 0 4px; } | ||
| .nav a { display:block; padding:4px 8px; color:var(--ink); text-decoration:none; border-radius:5px; font-size:13.5px; } | ||
| .nav a:hover { background:var(--code); } | ||
| .nav a.active { background:var(--accent); color:#0b0e14; } | ||
| .nav a .ty { float:right; font-size:10px; color:var(--muted); } | ||
| .nav a.active .ty { color:#0b0e14; } | ||
| #main { flex:1; max-width:900px; padding:32px 44px; } | ||
| #content h1,#content h2,#content h3 { line-height:1.25; } | ||
| #content h1 { font-size:28px; border-bottom:1px solid var(--border); padding-bottom:8px; } | ||
| #content a { color:var(--accent); } | ||
| #content code { background:var(--code); padding:2px 5px; border-radius:4px; font-size:90%; } | ||
| #content pre { background:var(--code); border:1px solid var(--border); border-radius:8px; padding:12px 14px; overflow:auto; } | ||
| #content pre code { background:none; padding:0; } | ||
| #content pre.mermaid-src { border-left:3px solid var(--accent); } | ||
| #content pre.mermaid-src::before { content:"⬡ Mermaid diagram (source)"; display:block; color:var(--muted); font-size:11px; margin-bottom:6px; } | ||
| #content table { border-collapse:collapse; width:100%; margin:14px 0; font-size:13.5px; } | ||
| #content th,#content td { border:1px solid var(--border); padding:6px 9px; text-align:left; vertical-align:top; } | ||
| #content th { background:var(--code); } | ||
| #content blockquote { border-left:3px solid var(--border); margin:12px 0; padding:2px 14px; color:var(--muted); } | ||
| #content hr { border:none; border-top:1px solid var(--border); margin:22px 0; } | ||
| .crumb { color:var(--muted); font-size:12px; margin-bottom:8px; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div id="app"> | ||
| <nav id="side"> | ||
| <h1>Knowledge Bundle</h1> | ||
| <input id="search" placeholder="Search…" autocomplete="off"> | ||
| <div id="nav" class="nav"></div> | ||
| </nav> | ||
| <main id="main"><div id="content"></div></main> | ||
| </div> | ||
| <script> | ||
| HTML_HEAD | ||
| # Inline page data: PAGES[rel] = {title, type, md}, plus ORDER (index first). | ||
| { | ||
| echo "const PAGES = {" | ||
| while IFS= read -r -d '' page; do | ||
| local rel title type | ||
| rel="${page#"$BUNDLE/"}" | ||
| title="$(get_yaml_field "$page" title)"; [[ -n "$title" ]] || title="$rel" | ||
| type="$(get_yaml_field "$page" type)" | ||
| printf '%s: {"title": %s, "type": %s, "md": %s},\n' \ | ||
| "$(jq -Rn --arg v "$rel" '$v')" \ | ||
| "$(jq -Rn --arg v "$title" '$v')" \ | ||
| "$(jq -Rn --arg v "$type" '$v')" \ | ||
| "$(strip_frontmatter "$page" | jq -Rs . | sed 's#</#<\\/#g')" | ||
| done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z) | ||
| echo "};" | ||
| # ORDER: bundle root index.md first, then everything else sorted. | ||
| echo "const ORDER = Object.keys(PAGES).sort(function(a,b){" | ||
| echo " if(a==='index.md') return -1; if(b==='index.md') return 1;" | ||
| echo " return a<b?-1:a>b?1:0; });" | ||
| } >>"$tmp" | ||
| cat >>"$tmp" <<'HTML_TAIL' | ||
| function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');} | ||
| function resolve(base, href){ | ||
| if(/^[a-z]+:\/\//.test(href)||href[0]==='#') return href; | ||
| var dir = base.indexOf('/')<0 ? '' : base.replace(/\/[^/]*$/,''); | ||
| var parts = (dir? dir.split('/'):[]).concat(href.split('/')), out=[]; | ||
| for(var i=0;i<parts.length;i++){ var p=parts[i]; | ||
| if(p==='..') out.pop(); else if(p!=='.'&&p!=='') out.push(p); } | ||
| return out.join('/'); | ||
| } | ||
| function inline(s, base){ | ||
| s = s.replace(/`([^`]+)`/g, function(m,c){return '<code>'+esc(c)+'</code>';}); | ||
| s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, function(m,t,u){ | ||
| if(/^[a-z]+:\/\//.test(u)) return '<a href="'+u+'" target="_blank" rel="noopener">'+t+'</a>'; | ||
| var key=resolve(base,u); | ||
| if(PAGES[key]) return '<a href="#'+key+'" data-nav="'+key+'">'+t+'</a>'; | ||
| return '<span title="'+esc(u)+'">'+t+'</span>'; | ||
| }); | ||
| s = s.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>'); | ||
| s = s.replace(/(^|[^*])\*([^*\n]+)\*/g,'$1<em>$2</em>'); | ||
| return s; | ||
| } | ||
| function render(md, base){ | ||
| // Pull fenced code blocks out first so their contents aren't block-parsed. | ||
| var blocks=[], src=md.replace(/```(\w*)\n([\s\S]*?)```/g,function(m,lang,body){ | ||
| var cls = lang==='mermaid' ? ' class="mermaid-src"' : ''; | ||
| blocks.push('<pre'+cls+'><code>'+esc(body.replace(/\n$/,''))+'</code></pre>'); | ||
| return ' |