🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@drafthq/draft

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@drafthq/draft - npm Package Compare versions

Comparing version
3.2.1
to
3.3.0
+28
.cursor-plugin/plugin.json
{
"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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
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 'BLOCK'+(blocks.length-1)+'';
});
var lines=src.split('\n'), out='', i=0, list='', tbl=[];
function closeList(){ if(list){ out+='</'+list+'>'; list=''; } }
function flushTbl(){
if(!tbl.length) return;
var rows=tbl.filter(function(r){return !/^\s*\|?[\s:|-]+\|?\s*$/.test(r);});
out+='<table>';
rows.forEach(function(r,ri){
var cells=r.replace(/^\||\|$/g,'').split('|');
out+='<tr>'+cells.map(function(c){var t=ri===0?'th':'td';return '<'+t+'>'+inline(c.trim(),base)+'</'+t+'>';}).join('')+'</tr>';
});
out+='</table>'; tbl=[];
}
for(;i<lines.length;i++){
var ln=lines[i];
if(/^\s*\|.*\|\s*$/.test(ln)){ closeList(); tbl.push(ln); continue; } else flushTbl();
var h=ln.match(/^(#{1,6})\s+(.*)$/);
if(h){ closeList(); out+='<h'+h[1].length+'>'+inline(esc(h[2]),base)+'</h'+h[1].length+'>'; continue; }
if(/^\s*---\s*$/.test(ln)){ closeList(); out+='<hr>'; continue; }
if(/^\s*>\s?/.test(ln)){ closeList(); out+='<blockquote>'+inline(esc(ln.replace(/^\s*>\s?/,'')),base)+'</blockquote>'; continue; }
var li=ln.match(/^\s*([-*]|\d+\.)\s+(.*)$/);
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; }
var b=ln.match(/^BLOCK(\d+)$/);
if(b){ closeList(); out+=blocks[+b[1]]; continue; }
if(/^\s*$/.test(ln)){ closeList(); continue; }
closeList(); out+='<p>'+inline(esc(ln),base)+'</p>';
}
flushTbl(); closeList();
return out;
}
var navEl=document.getElementById('nav'), contentEl=document.getElementById('content');
function section(k){ return k.indexOf('/')<0 ? '(root)' : k.split('/')[0]; }
function buildNav(filter){
navEl.innerHTML=''; var lastSec=null;
ORDER.forEach(function(k){
var p=PAGES[k];
if(filter && (p.title+' '+p.md).toLowerCase().indexOf(filter)<0) return;
var sec=section(k);
if(sec!==lastSec){ var s=document.createElement('div'); s.className='sec'; s.textContent=sec; navEl.appendChild(s); lastSec=sec; }
var a=document.createElement('a'); a.href='#'+k; a.dataset.nav=k;
a.innerHTML=esc(p.title)+(p.type?'<span class="ty">'+esc(p.type)+'</span>':'');
navEl.appendChild(a);
});
}
function show(k){
var p=PAGES[k]; if(!p){ k=ORDER[0]; p=PAGES[k]; }
contentEl.innerHTML='<div class="crumb">'+esc(k)+'</div>'+render(p.md,k);
document.querySelectorAll('#nav a').forEach(function(a){ a.classList.toggle('active', a.dataset.nav===k); });
if(location.hash.slice(1)!==k) history.replaceState(null,'','#'+k);
contentEl.parentElement.scrollTop=0; window.scrollTo(0,0);
}
document.addEventListener('click',function(e){ var a=e.target.closest('[data-nav]'); if(a){ e.preventDefault(); show(a.dataset.nav); } });
document.getElementById('search').addEventListener('input',function(e){ buildNav(e.target.value.toLowerCase().trim()); });
window.addEventListener('hashchange',function(){ var k=decodeURIComponent(location.hash.slice(1)); if(PAGES[k]) show(k); });
buildNav('');
show(decodeURIComponent(location.hash.slice(1)) || ORDER[0]);
</script>
</body>
</html>
HTML_TAIL
mkdir -p "$(dirname "$out")"
mv "$tmp" "$out"
echo "rendered offline HTML viewer → $out ($(find "$BUNDLE" -type f -name '*.md' | grep -c .) pages)"
}
[[ -n "$ARCH_OUT" ]] && render_architecture "$ARCH_OUT"
if [[ ${#CMAP_INTO[@]} -gt 0 ]]; then
MAP_TMP="$(mktemp)"
build_concept_map >"$MAP_TMP"
for tgt in "${CMAP_INTO[@]}"; do
inject_concept_map "$tgt" "$MAP_TMP"
done
rm -f "$MAP_TMP"
fi
[[ -n "$WEB_OUT" ]] && render_web "$WEB_OUT"
[[ -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; }
exit 0
#!/usr/bin/env bash
# okf-validate.sh — validate an OKF (Open Knowledge Format) taxonomy bundle.
#
# This is the deterministic ground-truth verifier for the `/draft:init` OKF
# emitter (DRAFT_INIT_MODE=okf). It fails the build on dangling cross-links,
# missing/invalid frontmatter, and an incomplete path→concept index, so a
# page-by-page generation pass cannot ship a structurally broken bundle.
#
# Checks:
# 1. BUNDLE_DIR exists and contains a root index.md.
# 2. Every concept page (any *.md whose frontmatter declares `type:`) carries
# all required OKF frontmatter keys: type, title, description, resource.
# 3. Every declared `type` is in the frozen code-repo vocabulary (§4 of HLD).
# 4. Every relative markdown cross-link ( ](path.md) ) resolves to a file that
# exists inside the bundle. External (http/https/mailto) and pure-anchor
# (#frag) links are ignored.
# 5. (optional) --path-index FILE: every concept page referenced by the
# path→concept index exists in the bundle (no dangle, no stale rename).
#
# Usage:
# scripts/tools/okf-validate.sh <BUNDLE_DIR> [--path-index FILE] [--json]
#
# Exit codes: 0 valid, 1 invalid (diagnostics to stderr), 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"
# Frozen concept `type` vocabulary for code repos. Changing this churns every
# generated file, so it is versioned in the bundle (index.md: okf_types_version).
OKF_TYPES="Subsystem Module Feature Entrypoint API DataModel Dependency ADR Runbook"
BUNDLE=""
PATH_INDEX=""
JSON=0
usage() {
cat <<'EOF'
okf-validate.sh — validate an OKF taxonomy bundle (the /draft:init OKF emitter output).
Usage:
scripts/tools/okf-validate.sh <BUNDLE_DIR> [--path-index FILE] [--json]
Flags:
--path-index FILE Validate a path→concept index (JSON): every concept page it
names must exist in the bundle.
--json Emit a JSON summary instead of human diagnostics.
--help Show this help.
Exit 0 valid, 1 invalid, 2 bundle directory not found.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--path-index) PATH_INDEX="$2"; shift 2;;
--json) JSON=1; shift;;
--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
if [[ -z "$BUNDLE" ]]; then
usage >&2
exit 1
fi
if [[ ! -d "$BUNDLE" ]]; then
echo "ERROR: bundle directory not found: $BUNDLE" >&2
exit 2
fi
BUNDLE="${BUNDLE%/}"
ERRORS=()
PAGE_COUNT=0
CONCEPT_COUNT=0
add_error() { ERRORS+=("$1"); }
# Does the frozen vocabulary contain $1?
is_known_type() {
local t="$1"
[[ " $OKF_TYPES " == *" $t "* ]]
}
# --- 1. Root index ---
if [[ ! -f "$BUNDLE/index.md" ]]; then
add_error "missing bundle root: $BUNDLE/index.md"
fi
# --- 2/3. Per-page frontmatter + type vocabulary ---
while IFS= read -r -d '' page; do
PAGE_COUNT=$((PAGE_COUNT + 1))
rel="${page#"$BUNDLE/"}"
# A page is a "concept" only if its frontmatter declares a type.
type_val="$(get_yaml_field "$page" "type")"
[[ -z "$type_val" ]] && continue
CONCEPT_COUNT=$((CONCEPT_COUNT + 1))
for key in title description resource; do
if [[ -z "$(get_yaml_field "$page" "$key")" ]]; then
add_error "$rel: concept missing required frontmatter field '$key'"
fi
done
if ! is_known_type "$type_val"; then
add_error "$rel: unknown concept type '$type_val' (frozen vocab: $OKF_TYPES)"
fi
done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
# --- 4. Cross-link resolution ---
# Scan every markdown page for relative links of the form ](target.md[#frag]).
# Resolve targets relative to the linking file's directory; flag dangles.
# Use a temp file (outside the bundle) because the scan runs in a pipeline
# subshell where add_error would not persist.
DANGLE_FILE="$(mktemp)"
trap 'rm -f "$DANGLE_FILE"' EXIT
while IFS= read -r -d '' page; do
pdir="$(dirname "$page")"
prel="${page#"$BUNDLE/"}"
# Extract link targets: text inside ]( ... ) up to a space or closing paren.
grep -oE '\]\([^) ]+\)' "$page" 2>/dev/null | sed -E 's/^\]\(//; s/\)$//' | while IFS= read -r target; do
[[ -z "$target" ]] && continue
# Skip external schemes and pure anchors.
case "$target" in
http://*|https://*|mailto:*|\#*) continue;;
esac
# Strip any #anchor and ?query.
target="${target%%#*}"
target="${target%%\?*}"
[[ -z "$target" ]] && continue
# Only resolve intra-bundle markdown/asset links (relative paths).
case "$target" in
/*) continue;; # absolute path — out of scope for bundle integrity
esac
resolved="$pdir/$target"
if [[ ! -e "$resolved" ]]; then
printf '%s\t%s\n' "$prel" "$target"
fi
done || true # inner pipeline returns non-zero at EOF; don't trip set -e
done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z) >>"$DANGLE_FILE"
if [[ -s "$DANGLE_FILE" ]]; then
while IFS=$'\t' read -r prel target; do
add_error "$prel: dangling cross-link → '$target'"
done < "$DANGLE_FILE"
fi
# --- 5. path→concept index completeness (optional) ---
if [[ -n "$PATH_INDEX" ]]; then
if [[ ! -f "$PATH_INDEX" ]]; then
add_error "path-index not found: $PATH_INDEX"
else
# The index maps source path → array of concept page(s), bundle-relative:
# { "src/auth/login.go": ["systems/auth.md"], ... }
# Validate the array VALUES (the pages) only. Keys are source paths and may
# themselves end in .md (e.g. grounding to docs/INVARIANTS.md) — those are
# not bundle pages, so we extract strings *inside* the [ ... ] value arrays
# and ignore keys entirely. Each page must exist in the bundle.
while IFS= read -r ref; do
[[ -z "$ref" ]] && continue
if [[ ! -f "$BUNDLE/$ref" ]]; then
add_error "path-index references missing concept page: $ref"
fi
done < <(grep -oE '\[[^]]*\]' "$PATH_INDEX" 2>/dev/null \
| grep -oE '"[^"]+\.md"' | tr -d '"' | sort -u)
fi
fi
# --- Report ---
if [[ $JSON -eq 1 ]]; then
valid=true
[[ ${#ERRORS[@]} -eq 0 ]] || valid=false
printf '{"valid":%s,"bundle":"%s","pages":%d,"concepts":%d,"errors":[' \
"$valid" "$(json_escape "$BUNDLE")" "$PAGE_COUNT" "$CONCEPT_COUNT"
if [[ ${#ERRORS[@]} -gt 0 ]]; then
for i in "${!ERRORS[@]}"; do
[[ $i -gt 0 ]] && printf ','
printf '"%s"' "$(json_escape "${ERRORS[$i]}")"
done
fi
printf ']}\n'
else
if [[ ${#ERRORS[@]} -gt 0 ]]; then
echo "OKF bundle INVALID: $BUNDLE ($PAGE_COUNT pages, $CONCEPT_COUNT concepts)" >&2
for e in "${ERRORS[@]}"; do
echo " - $e" >&2
done
else
echo "OKF bundle valid: $BUNDLE ($PAGE_COUNT pages, $CONCEPT_COUNT concepts)"
fi
fi
[[ ${#ERRORS[@]} -eq 0 ]] || exit 1
exit 0
## Init — OKF Taxonomy Emitter (DRAFT_INIT_MODE=okf)
> Progressive-disclosure reference for `/draft:init`. Covers the OKF emitter:
> type vocabulary, frontmatter contract, generation pipeline, render views,
> incremental refresh, and the `okf-validate.sh` gate. Authoritative HLD:
> `hld-draft-init-okf-taxonomy.md` at the repo root.
This is the `/draft:init` output mode for **tier 3+ repos**, where it is the
**tier-gated default** (`DRAFT_INIT_MODE` unset → tier 1–2 `monolith`, tier 3–5
`okf`; an explicit `DRAFT_INIT_MODE=monolith|okf` overrides). `monolith` is
retained — the tier-1/2 default, the A/B baseline, and the over-fetch fallback.
The default rests on maintainability/readability, not the benchmark (parity).
```bash
# Mode gate — default is tier-gated 'auto', finalized after Step 1.4.5 (tier):
DRAFT_INIT_MODE="${DRAFT_INIT_MODE:-auto}"
case "$DRAFT_INIT_MODE" in
monolith|okf) : ;; # explicit override — honored as-is
auto) : ;; # resolve from tier: 1–2 → monolith, 3–5 → okf
*) echo "Unknown DRAFT_INIT_MODE='$DRAFT_INIT_MODE' (monolith|okf|auto); using auto" >&2
DRAFT_INIT_MODE=auto ;;
esac
```
Everything else in `/draft:init` (5-phase analysis, graph snapshot, `.state/`
hashing, scope detection, atomic staging) is **reused unchanged**. This mode adds
a decomposition + serialization stage. It introduces **no new LLM analysis
engine** and exactly **one** new deterministic helper, `okf-validate.sh`.
## Target layout
`okf` mode changes **only** the `architecture.md` / `.ai-context.md` packaging
and adds `wiki/`. **Every other standard `/draft:init` file is still produced**
— `product.md`, `tech-stack.md`, `workflow.md`, `guardrails.md`, `index.md`,
`.ai-profile.md`, `tracks/` + `tracks.md`, `.state/`, `graph/` — exactly as in
`monolith` mode. Do **not** skip them: emitting only the bundle is a regression.
```
draft/
├── .ai-context.md # INDEX ROOT: synopsis (150–250 lines) + Concept Map
├── architecture.md # RENDERED VIEW (generated from bundle; not source of truth)
├── .ai-profile.md # always-injected profile (derived from .ai-context.md) [SAME AS MONOLITH]
├── product.md # [SAME AS MONOLITH]
├── tech-stack.md # [SAME AS MONOLITH]
├── workflow.md # [SAME AS MONOLITH]
├── guardrails.md # [SAME AS MONOLITH]
├── index.md # docs index — lists prose files + wiki/ [SAME AS MONOLITH, +wiki link]
├── tracks.md + tracks/ # [SAME AS MONOLITH]
├── wiki/ # OKF bundle (source of truth) — okf-mode ONLY
│ ├── index.md # bundle root + Concept Map
│ ├── overview/{index,architecture,getting-started,glossary}.md
│ ├── systems/{index,<subsystem>}.md
│ ├── features/{index,<feature>}.md
│ ├── reference/{index,<ref>}.md # config, deps, data models, ADRs, runbooks
│ ├── entrypoints/<app>.md
│ ├── web/index.html # optional offline viewer (okf-render-views.sh --web)
│ └── log.md # chronological change history (from .state run memory)
├── graph/schema.yaml # [SAME AS MONOLITH] engine gate marker
└── .state/
├── hashes.json # file → content hash [SAME AS MONOLITH]
├── path-to-concept.json # NEW: source path → concept page(s) it grounds
└── signals.json # [SAME AS MONOLITH]
```
The standard project files come from the same generators as `monolith` mode
(intake questions → `product.md`; tech detection → `tech-stack.md`; `/draft:learn`
→ `guardrails.md`; templates → `workflow.md`, `index.md`, `tracks.md`,
`.ai-profile.md`). The OKF emitter only *replaces the architecture packaging*; it
never owns or removes the rest of the context directory.
Templates for each bundle page live in `core/templates/okf/` (`index.md`,
`concept.md`, `section-index.md`, `ai-context-index.md`).
## Frozen `type` vocabulary
Every concept carries a `type` from this frozen set (changing it churns every
file; versioned via `index.md` frontmatter `okf_types_version`):
| type | Maps to | Home |
|------|---------|------|
| `Subsystem` | major graph cluster / package boundary | `systems/` |
| `Module` | single package/dir, 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/` |
`okf-validate.sh` enforces this set as ground truth — an out-of-vocab `type`
fails the build.
## Frontmatter contract
Per concept page (see `core/templates/okf/concept.md`). Required OKF keys:
`type`, `title`, `description`, `resource`. **`description` is the load-bearing
routing key** — write it as a routing decision ("should the agent open this for
the task at hand?"), never a summary. Draft extensions are namespaced `x-` and
ignored by generic OKF consumers: `x-grounded-paths`, `x-hotspot-score`,
`x-callers`.
## Concept granularity (resolves open decision 1)
Derive concepts from the graph, not by hand:
- A **Subsystem** = a graph cluster (package/dir boundary) with `fan_in ≥ 2` from
other clusters, OR a top-ranked module from `hotspot-rank.sh`.
- A **Module** = a cohesive package/dir below a subsystem that is *not* itself a
cluster boundary but has its own hotspot or public surface.
- A **Feature** = a capability the graph shows spanning ≥2 modules (shared
callers / a route group touching multiple packages).
- Default cap: one page per package boundary; do not split a package into
multiple concept pages unless it has >1 distinct public surface. This keeps
page count ≈ module count and navigation depth shallow.
## Generation pipeline (M3)
```
1. Survey → existing /draft:init 5-phase + graph snapshot (graph-snapshot.sh)
2. Plan → derive the concept list (above) from graph clusters +
entrypoints + features. Topo-sort by dependency so pages that
others link to (overview, core subsystems) generate FIRST —
forward cross-links resolve.
3. Generate → per concept, pull grounding from the graph and write the page:
x-callers ← graph-callers.sh --symbol <c>
x-grounded-paths ← graph-impact.sh --symbol <c> (blast radius)
x-hotspot-score ← hotspot-rank.sh
overview diagrams ← mermaid-from-graph.sh
Record each source path → page in .state/path-to-concept.json.
4. Render views → ai-context.md (synopsis + Concept Map), architecture.md
(concatenated view), wiki/log.md (see M4).
5. Validate → okf-validate.sh draft/wiki \
--path-index draft/.state/path-to-concept.json
FAIL the build (do not atomic-rename) on any dangle, missing
field, bad type, or path-index gap.
6. Emit → mv draft.tmp/ draft/ ; update .state/.
```
Page bodies are LLM-narrated for readability **but** the graph-derived
frontmatter and the `Blast radius`/`Used by` sections are deterministic. To keep
incremental carry-forward byte-identical (open decision 2), cache the narrated
prose keyed by the source hash of `x-grounded-paths` — unchanged sources reuse
the cached narration verbatim.
## Render views (M4)
Both are produced by the deterministic helper `okf-render-views.sh` (no LLM) —
regenerated on every init/refresh so they never drift from the bundle:
```bash
okf-render-views.sh draft/wiki \
--arch-out draft/architecture.md \
--concept-map-into draft/wiki/index.md \
--concept-map-into draft/.ai-context.md \
--web draft/wiki/web/index.html
```
- `--arch-out` renders the linear `architecture.md` (banner + TOC + every concept
page in canonical section order, frontmatter stripped, Mermaid preserved).
- `--concept-map-into` rebuilds the routing table between the
`<!-- CONCEPT-MAP:START -->` / `:END` markers from each concept's `title` +
`type` + `description` (section `index.md` pages excluded).
- `--web` writes a **self-contained offline HTML viewer** (single file: all pages
inlined as JSON + a built-in markdown renderer + sidebar nav + search). Works by
double-click — no server, no internet, no CDN. Optional, human-facing; regenerate
on refresh like the other views. (Mermaid blocks render as labeled source since a
graphical engine can't be inlined offline.)
All views write into `draft/` (the OKF emitter never creates a separate output
dir): `draft/wiki/` is the bundle, `draft/architecture.md` + `draft/.ai-context.md`
are the rendered views, `draft/wiki/web/index.html` is the optional viewer.
The two views:
- **`ai-context.md`** (index root) — from `core/templates/okf/ai-context-index.md`:
the Synopsis preserves the prior `.ai-context.md` content shape (so existing
downstream consumers keep working); the Concept Map is built from each
concept's `description`. Broad tasks terminate here.
- **`architecture.md`** (rendered view) — TOC + per-concept section concat +
Mermaid, in topo order. Demoted, not deleted: the brownfield Context Quality
Audit and any command grepping `architecture.md` keep working (§9 of the HLD).
- **`wiki/log.md`** — appended from `.state/` run memory.
The `<!-- CONCEPT-MAP:START -->` / `:END` markers in `wiki/index.md` and the
section `index.md` tables are the injection slots for the routing tables.
## Incremental refresh at concept granularity (M5)
`/draft:init refresh` under `okf` mode:
```
1. Diff hashes.json vs working tree → changed source paths
2. path-to-concept.json → affected concept pages
3. Regenerate ONLY affected concepts; carry the rest verbatim (cached narration)
4. Re-render ai-context.md / architecture.md / log.md (cheap; always regenerated)
5. Re-validate: okf-validate.sh on the bundle + path-index (cross-links touching
changed concepts must still resolve)
6. Append log.md; update hashes.json + path-to-concept.json
```
A 1-file change regenerates only the concept(s) that file grounds. Unchanged
concepts are byte-identical across runs.
## Backward compatibility (§9)
- `architecture.md` is retained as a rendered view (brownfield audit + grep keep
working).
- `ai-context.md`'s Synopsis preserves the prior content shape; the Concept Map
is additive.
- `/draft:review` and downstream command contracts are unchanged — they consume
`architecture.md` / `ai-context.md`, both of which still exist.
## Default policy & retirement of `monolith`
`okf` is the **tier-gated default** (tier 3+); `monolith` is the tier-1/2 default
and remains in place as the A/B baseline + over-fetch fallback. Full retirement of
`monolith` is deferred until **both**: (1) the large-monolith A/B run shows `okf` ≥
baseline on tokens at parity accuracy (the regime the §12 benchmark flagged), and
(2) a human-onboarding eval confirms the wiki + generated `architecture.md` covers
linear onboarding. Note: retiring `monolith` would not remove `architecture.md` —
it is generated from the bundle regardless — so there is no readability gain from
deletion, only lost optionality.
+1
-1

@@ -15,3 +15,3 @@ {

"description": "Context-Driven Development: draft specs and plans before implementation. Structured workflows for features and fixes.",
"version": "3.2.1",
"version": "3.3.0",
"author": {

@@ -18,0 +18,0 @@ "name": "mayurpise"

{
"name": "draft",
"description": "Context-Driven Development: draft specs and plans before implementation. Structured workflows for features and fixes.",
"version": "3.2.1",
"version": "3.3.0",
"author": {

@@ -6,0 +6,0 @@ "name": "mayurpise"

@@ -5,7 +5,11 @@ 'use strict';

const { asset } = require('../lib/paths');
const { readPluginVersion } = require('../lib/plugin-manifest');
const { applyCursorRegistration } = require('../lib/cursor-registry');
// Cursor natively understands the .claude-plugin structure. Install the same
// native plugin tree, by default to the user-level plugin directory so it is
// available across all projects.
// Cursor discovers Draft through two layers, both shipped here: the native
// .cursor-plugin/plugin.json manifest, and the shared Claude plugin registry
// under ~/.claude/ (written in postInstall). A file copy alone is not enough —
// current Cursor builds never surface /draft:* commands without registration.
const ITEMS = [
{ p: '.cursor-plugin', kind: 'copyTree' },
{ p: '.claude-plugin', kind: 'copyTree' },

@@ -41,3 +45,3 @@ { p: 'skills', kind: 'copyTree' },

}));
// Guard the whole install dir on the manifest's presence.
// Guard the whole install dir on the .cursor-plugin manifest's presence.
actions[0].guard = true;

@@ -49,5 +53,31 @@

graph: true,
done: `Draft installed to ${base}. Restart Cursor to detect the plugin.`,
// Runs after the file copies: register + enable the plugin in the shared
// Claude registry. On a dry run it computes the merges and writes nothing.
postInstall(c) {
const installedManifest = path.join(base, '.cursor-plugin', 'plugin.json');
const version = readPluginVersion(
c.dryRun ? asset('.cursor-plugin', 'plugin.json') : installedManifest
);
return applyCursorRegistration({
home: c.home,
installPath: base,
version,
scope: c.scope,
dryRun: c.dryRun,
});
},
done: [
`Draft installed to ${base}.`,
'Plugin registered and enabled in ~/.claude/plugins/.',
'Restart Cursor (Developer: Reload Window) to load /draft:* commands.',
].join(' '),
fallbackTitle: 'If /draft commands do not appear after restart:',
fallback: [
`Confirm ${base}/.cursor-plugin/plugin.json exists`,
'Confirm ~/.claude/plugins/installed_plugins.json contains "draft@draft-plugins"',
'Confirm ~/.claude/settings.json has "draft@draft-plugins": true',
'Run Developer: Reload Window in Cursor',
],
};
},
};

@@ -111,2 +111,14 @@ 'use strict';

// Host-specific registration (e.g. Cursor's plugin registry). On a real
// install the hook performs the writes (logging each path); on a dry run it
// returns the planned paths so we can surface them without touching disk.
if (plan.postInstall) {
const reg = plan.postInstall(ctx);
if (reg && ctx.dryRun) {
log.plan(`would update registry: ${reg.kmPath}`);
log.plan(`would update registry: ${reg.ipPath}`);
log.plan(`would update registry: ${reg.settingsPath}`);
}
}
// Record the install path so skills can locate scripts/tools/ from the user's

@@ -113,0 +125,0 @@ // project cwd (best-effort; graph skills glob-fallback if the marker is absent).

@@ -303,3 +303,3 @@ # Draft Methodology

Draft works with **Claude Code** (native `.claude-plugin/` support) and **Cursor** (supports `.claude/` plugin structure natively). No build pipeline required.
Draft works with **Claude Code** (native `.claude-plugin/` support) and **Cursor** (requires `.cursor-plugin/plugin.json` plus registration in the shared Claude plugin registry, both handled by `draft install cursor`). No build pipeline required.

@@ -306,0 +306,0 @@ ---

{
"name": "@drafthq/draft",
"version": "3.2.1",
"version": "3.3.0",
"description": "Context-Driven Development for AI coding agents — install Draft into Claude Code, Cursor, Codex, or opencode.",

@@ -17,2 +17,3 @@ "bin": {

".claude-plugin/",
".cursor-plugin/",
"integrations/",

@@ -28,3 +29,3 @@ "bin/",

"test": "bash tests/test-cli.sh",
"version": "bash scripts/sync-version.sh && git add .claude-plugin/plugin.json .claude-plugin/marketplace.json web/index.html",
"version": "bash scripts/sync-version.sh && git add .claude-plugin/plugin.json .claude-plugin/marketplace.json .cursor-plugin/plugin.json",
"prepublishOnly": "bash scripts/build-integrations.sh"

@@ -31,0 +32,0 @@ },

@@ -70,3 +70,3 @@ <h1 align="center">Draft</h1>

| **Claude Code** | `claude-code` | Registers the plugin via `claude plugin marketplace add` + `claude plugin install` (user scope). Restart Claude Code. |
| **Cursor** | `cursor` | Copies the plugin into `~/.cursor/plugins/local/draft/` (auto-loaded). Restart Cursor. |
| **Cursor** | `cursor` | Copies the plugin into `~/.cursor/plugins/local/draft/`, writes `.cursor-plugin/plugin.json`, registers `draft@draft-plugins` in Cursor's plugin registry, and enables it. Restart Cursor (or Developer: Reload Window). |
| **Codex** | `codex` | Writes `./AGENTS.md`, which Codex reads automatically. |

@@ -96,3 +96,3 @@ | **opencode** | `opencode` | Writes `./AGENTS.md` + `~/.agents/skills/draft/`, both auto-discovered. |

### Cursor — from GitHub
Cursor natively supports the `.claude-plugin/` structure. Add via *Settings > Rules, Skills, Subagents > Rules > New > Add from Github*:
Cursor requires `.cursor-plugin/plugin.json`; the `draft install cursor` command also registers the plugin via the shared Claude plugin registry that Cursor reads on many builds. To add from source instead, use *Settings > Rules, Skills, Subagents > Rules > New > Add from Github*:
```

@@ -99,0 +99,0 @@ https://github.com/drafthq/draft.git

@@ -117,2 +117,7 @@ #!/usr/bin/env bash

"templates/lld.md"
# OKF taxonomy bundle templates (DRAFT_INIT_MODE=okf)
"templates/okf/index.md"
"templates/okf/concept.md"
"templates/okf/section-index.md"
"templates/okf/ai-context-index.md"
# Agents

@@ -162,2 +167,3 @@ "agents/architect.md"

"graph-init.sh"
"graph-preflight.sh"
"graph-impact.sh"

@@ -192,2 +198,5 @@ "graph-callers.sh"

"resolve-tools.sh"
# OKF taxonomy emitter (DRAFT_INIT_MODE=okf)
"okf-validate.sh"
"okf-render-views.sh"
)

@@ -194,0 +203,0 @@

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display