🎩 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.5.3
to
3.6.0
+256
scripts/tools/okf-emit-catalog.sh
#!/usr/bin/env bash
# okf-emit-catalog.sh — deterministically emit minimum-viable concept pages for
# every REQUIRED entry in a concept-plan.json that does not yet have a page.
#
# Purpose: XL monorepos cannot rely on the LLM to narrate every Module page in
# one shot. This tool writes quality-gate-passing catalog pages from plan
# metadata + optional Cargo/README crumbs so init can complete without gaps.
# Agents may later enrich top-N hotspots; the catalog is the completeness floor.
#
# Usage:
# okf-emit-catalog.sh --plan FILE --bundle DIR [--repo DIR] [--force]
#
# Exit: 0 ok, 1 error, 2 plan/bundle missing.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/tools/_lib.sh
source "$SCRIPT_DIR/_lib.sh"
PLAN=""
BUNDLE=""
REPO="."
FORCE=0
usage() {
cat <<'EOF'
okf-emit-catalog.sh — write minimum-viable concept pages for missing plan entries.
Usage:
okf-emit-catalog.sh --plan FILE --bundle DIR [--repo DIR] [--force]
Flags:
--plan FILE concept-plan.json (required)
--bundle DIR wiki/ bundle directory (required)
--repo DIR repository root for grounding (default: .)
--force Overwrite existing pages (default: skip if present)
--help Show help
Exit: 0 ok, 1 error, 2 missing plan/bundle.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--plan) PLAN="${2:?--plan requires a value}"; shift 2;;
--bundle) BUNDLE="${2:?--bundle requires a value}"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--force) FORCE=1; shift;;
--help|-h) usage; exit 0;;
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
*) echo "Unexpected arg: $1" >&2; exit 1;;
esac
done
[[ -n "$PLAN" && -f "$PLAN" ]] || { echo "ERROR: --plan required" >&2; exit 2; }
[[ -n "$BUNDLE" && -d "$BUNDLE" ]] || { echo "ERROR: --bundle directory required" >&2; exit 2; }
[[ -d "$REPO" ]] || { echo "ERROR: --repo is not a directory" >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "ERROR: jq required" >&2; exit 1; }
BUNDLE="${BUNDLE%/}"
REPO="$(cd "$REPO" && pwd)"
TS="${OKF_CATALOG_TS:-$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "1970-01-01T00:00:00Z")}"
wrote=0
skipped=0
# Best-effort one-line description from resource path.
sniff_desc() {
local res="$1" lib
lib="$REPO/$res/src/lib.rs"
[[ -f "$lib" ]] || lib="$REPO/$res/lib.rs"
if [[ -f "$lib" ]]; then
awk '/^\/\/!/ { sub(/^\/\/![[:space:]]?/, ""); print; exit }' "$lib"
return
fi
if [[ -f "$REPO/$res/README.md" ]]; then
awk 'NF && !/^#/ { print; exit }' "$REPO/$res/README.md"
return
fi
printf 'Workspace component at %s.' "$res"
}
write_page() {
local cid="$1" ctype="$2" resource="$3" fan_in="$4"
local out="$BUNDLE/$cid"
mkdir -p "$(dirname "$out")"
if [[ -f "$out" && $FORCE -eq 0 ]]; then
skipped=$((skipped+1))
return
fi
local title stem section
stem="$(basename "$cid" .md)"
section="$(dirname "$cid")"
title="$stem"
local desc
desc="$(sniff_desc "$resource")"
[[ -n "$desc" ]] || desc="Open when changing the ${stem} component at ${resource}."
# Routing description (load-bearing for concept map).
local routing="Open when changing \`${stem}\` (${resource}). ${desc}"
routing="$(printf '%s' "$routing" | tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g' | cut -c1-280)"
local g1 g2
g1="$resource"
[[ -e "$REPO/$resource" ]] || g1="README.md"
if [[ -f "$REPO/$resource/src/lib.rs" ]]; then
g1="$resource/src/lib.rs"
elif [[ -f "$REPO/$resource/Cargo.toml" ]]; then
g1="$resource/Cargo.toml"
elif [[ -f "$REPO/$resource/package.json" ]]; then
g1="$resource/package.json"
elif [[ -f "$REPO/$resource/go.mod" ]]; then
g1="$resource/go.mod"
fi
g2="README.md"
[[ -f "$REPO/README.md" ]] || g2="Cargo.toml"
[[ -f "$REPO/$g2" || -f "$REPO/Cargo.toml" ]] || g2="."
# Types that need mermaid + full sections
local needs_diagram=0
case "$ctype" in
Subsystem|Module|Feature|Entrypoint) needs_diagram=1;;
esac
{
echo "---"
echo "type: $ctype"
echo "title: \"$title\""
echo "description: >"
echo " $routing"
echo "resource: \"$resource\""
echo "tags: [catalog, auto-emitted]"
echo "timestamp: \"$TS\""
echo "x-grounded-paths: [\"$g1\", \"$g2\"]"
echo "x-hotspot-score: 0.0"
echo "x-callers: []"
echo "x-catalog: true"
echo "---"
echo ""
echo "# $title"
echo ""
echo "## What it is"
echo ""
echo "Catalog entry for **\`${stem}\`** at \`${resource}\` (auto-emitted from the concept plan)."
echo ""
echo "$desc"
echo ""
echo "Fan-in (plan): ${fan_in}. Enrich this page during deep analysis if it is a hotspot."
echo ""
if [[ $needs_diagram -eq 1 ]]; then
echo "## How it works"
echo ""
echo "Primary implementation lives under \`${resource}\`. Prefer \`cargo check -p ${stem}\` /"
echo "package-local tests when iterating. Full control-flow diagrams belong in a later deep-dive."
echo ""
echo '```mermaid'
echo "flowchart TB"
echo " Consumer[Downstream consumers] --> C[\"${stem}\"]"
echo " C --> Impl[\"${resource}\"]"
echo '```'
echo ""
echo "## Used by"
echo ""
echo "- Parent wiki indexes under \`${section}/\`"
echo "- Workspace members that depend on \`${stem}\` (see package manifest)"
echo ""
echo "## Blast radius"
echo ""
echo "Changes to public APIs in \`${resource}\` may break dependent packages in this monorepo."
echo "Run targeted tests for \`${stem}\` and re-check direct reverse dependents."
echo ""
echo "## See also"
echo ""
echo "- [section index](index.md)"
echo "- Repository README / architecture overview"
echo ""
echo "## Notes"
echo ""
echo "- Emitted by \`okf-emit-catalog.sh\` so completeness does not depend on LLM coverage."
echo "- Safe to overwrite with a richer concept page on refresh/deep-dive (\`--force\`)."
else
echo "## How it works"
echo ""
echo "See \`${resource}\` and related package docs."
echo ""
echo "## See also"
echo ""
echo "- [section index](index.md)"
fi
} > "$out"
wrote=$((wrote+1))
}
while IFS=$'\t' read -r cid ctype resource fan_in required; do
[[ -z "$cid" ]] && continue
[[ "$required" == "true" ]] || continue
write_page "$cid" "${ctype:-Module}" "${resource:-.}" "${fan_in:-0}"
done < <(jq -r '.expected[] | [.concept_id, (.type // "Module"), (.resource // "."), ((.fan_in // 0)|tostring), (.required|tostring)] | @tsv' "$PLAN")
# Ensure section index stubs exist so render/section-indexes can run.
for sec in overview systems features reference entrypoints; do
dir="$BUNDLE/$sec"
[[ -d "$dir" ]] || continue
if [[ ! -f "$dir/index.md" ]]; then
cat > "$dir/index.md" <<EOF
---
title: "$sec"
---
# $sec
## Concept Map
<!-- CONCEPT-MAP:START -->
<!-- CONCEPT-MAP:END -->
EOF
fi
done
# Bundle root index if missing — only link section dirs that exist (no dangling rows).
if [[ ! -f "$BUNDLE/index.md" ]]; then
{
echo "---"
echo "type: Subsystem"
echo "title: Project Wiki"
echo "description: >"
echo " Root index of the project wiki. Start here, then route via the Concept Map."
echo "resource: ."
echo "tags: [index]"
echo "timestamp: \"$TS\""
echo "okf_version: \"0.1\""
echo "okf_types_version: \"0.1\""
echo "---"
echo ""
echo "# Project Wiki"
echo ""
echo "## Sections"
echo ""
echo "| Section | Index |"
echo "|---------|-------|"
for sec in overview systems features reference entrypoints; do
if [[ -d "$BUNDLE/$sec" && -f "$BUNDLE/$sec/index.md" ]]; then
echo "| $sec | [$sec/index.md]($sec/index.md) |"
fi
done
echo ""
echo "## Concept Map"
echo ""
echo "<!-- CONCEPT-MAP:START -->"
echo "<!-- CONCEPT-MAP:END -->"
} > "$BUNDLE/index.md"
fi
echo "okf-emit-catalog: wrote=$wrote skipped_existing=$skipped plan=$PLAN bundle=$BUNDLE"
exit 0
#!/usr/bin/env bash
# okf-fix-links.sh — rewrite and validate markdown links in draft/ views.
#
# Fixes the class of bugs seen when okf-render-views concatenates wiki pages into
# draft/architecture.md (sibling-relative links and ambiguous basenames break).
# Also validates the whole draft/ tree (or a single file).
#
# Usage:
# okf-fix-links.sh --draft DIR [--fix] [--check]
# okf-fix-links.sh --file FILE --wiki DIR [--fix]
#
# Exit: 0 all links resolve (after optional fix), 1 broken links remain, 2 usage.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/tools/_lib.sh
source "$SCRIPT_DIR/_lib.sh"
DRAFT=""
FILE=""
WIKI=""
DO_FIX=0
DO_CHECK=1
usage() {
cat <<'EOF'
okf-fix-links.sh — fix/validate markdown links in Draft OKF outputs.
Usage:
okf-fix-links.sh --draft DIR [--fix] [--check]
okf-fix-links.sh --file architecture.md --wiki DIR/wiki [--fix]
Flags:
--draft DIR draft/ directory (architecture.md + wiki/ + .ai-context.md)
--file FILE single markdown file to rewrite/check
--wiki DIR wiki bundle (required with --file for basename map)
--fix rewrite broken/ambiguous links in place
--check report broken links (default on)
--help
Exit: 0 clean, 1 broken links remain, 2 bad invocation.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--draft) DRAFT="${2:?--draft requires a value}"; shift 2;;
--file) FILE="${2:?--file requires a value}"; shift 2;;
--wiki) WIKI="${2:?--wiki requires a value}"; shift 2;;
--fix) DO_FIX=1; shift;;
--check) DO_CHECK=1; shift;;
--help|-h) usage; exit 0;;
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 2;;
*) echo "Unexpected arg: $1" >&2; exit 2;;
esac
done
if [[ -n "$DRAFT" ]]; then
DRAFT="${DRAFT%/}"
[[ -d "$DRAFT" ]] || { echo "ERROR: --draft not a directory" >&2; exit 2; }
WIKI="${WIKI:-$DRAFT/wiki}"
FILE="${FILE:-$DRAFT/architecture.md}"
fi
[[ -n "$FILE" && -f "$FILE" ]] || { echo "ERROR: --file or --draft/architecture.md required" >&2; exit 2; }
[[ -n "$WIKI" && -d "$WIKI" ]] || { echo "ERROR: wiki dir required" >&2; exit 2; }
# Build basename → list of wiki-relative paths (relative to draft root as wiki/...)
declare -A BASENAME_MAP_MULTI=()
while IFS= read -r -d '' p; do
rel="wiki/${p#"$WIKI"/}"
base="$(basename "$p")"
if [[ -n "${BASENAME_MAP_MULTI[$base]:-}" ]]; then
BASENAME_MAP_MULTI[$base]="${BASENAME_MAP_MULTI[$base]}|$rel"
else
BASENAME_MAP_MULTI[$base]="$rel"
fi
done < <(find "$WIKI" -type f -name '*.md' -print0)
pick_target() {
local base="$1" label="$2"
local cands="${BASENAME_MAP_MULTI[$base]:-}"
[[ -n "$cands" ]] || { echo ""; return; }
if [[ "$cands" != *"|"* ]]; then
printf '%s' "$cands"
return
fi
local lab; lab="$(printf '%s' "$label" | tr '[:upper:]' '[:lower:]')"
IFS='|' read -ra arr <<< "$cands"
local pref
if [[ "$lab" == *product* || "$lab" == *feature* || "$lab" == *user*guide* || "$lab" == *install* ]]; then
for pref in "${arr[@]}"; do [[ "$pref" == *"/features/"* ]] && { printf '%s' "$pref"; return; }; done
fi
if [[ "$lab" == *build* || "$lab" == *runbook* || "$lab" == *ops* || "$lab" == *source* ]]; then
for pref in "${arr[@]}"; do [[ "$pref" == *"/overview/"* ]] && { printf '%s' "$pref"; return; }; done
fi
for order in /systems/ /features/ /overview/ /reference/ /entrypoints/; do
for pref in "${arr[@]}"; do
[[ "$pref" == *"$order"* ]] && { printf '%s' "$pref"; return; }
done
done
printf '%s' "${arr[0]}"
}
# Collect heading slugs from a file for anchor checks
build_slugs() {
local f="$1"
declare -gA SLUGS=()
local counts=()
# shellcheck disable=SC2034
while IFS= read -r line; do
[[ "$line" =~ ^#{1,6}[[:space:]]+(.*)$ ]] || continue
local raw="${BASH_REMATCH[1]}"
local s; s="$(gfm_slug "$raw")"
local n="${SLUGS[_count_$s]:-0}"
SLUGS[_count_$s]=$((n+1))
if [[ $n -eq 0 ]]; then
SLUGS[$s]=1
else
SLUGS["${s}-$n"]=1
fi
done < "$f"
}
resolve_ok() {
local src="$1" href="$2"
[[ "$href" =~ ^https?:// || "$href" =~ ^mailto: ]] && return 0
if [[ "$href" == \#* ]]; then
local a="${href#\#}"
[[ -n "${SLUGS[$a]:-}" ]] && return 0
return 1
fi
local path="${href%%\#*}"
[[ -z "$path" ]] && return 0
local dir; dir="$(cd "$(dirname "$src")" && pwd)"
local draft_root=""
if [[ -n "$DRAFT" ]]; then
draft_root="$(cd "$DRAFT" && pwd)"
else
draft_root="$(cd "$(dirname "$src")" && pwd)"
fi
[[ -e "$dir/$path" || -e "$draft_root/$path" || -e "$path" ]] && return 0
return 1
}
fix_file() {
local src="$1"
local tmp; tmp="$(mktemp)"
python3 - "$src" "$WIKI" "$tmp" <<'PY'
import sys, re
from pathlib import Path
src = Path(sys.argv[1])
wiki = Path(sys.argv[2])
out = Path(sys.argv[3])
text = src.read_text(encoding="utf-8", errors="replace")
# basename -> [wiki-relative draft paths]
bmap = {}
for p in wiki.rglob("*.md"):
rel = "wiki/" + str(p.relative_to(wiki)).replace("\\", "/")
bmap.setdefault(p.name, []).append(rel)
def pick(base, label):
cands = bmap.get(base, [])
if not cands:
return None
if len(cands) == 1:
return cands[0]
lab = label.lower()
def prefer(substr):
for c in cands:
if substr in c:
return c
return None
if any(k in lab for k in ("product", "feature", "user guide", "install", "onboarding")):
p = prefer("/features/")
if p: return p
if any(k in lab for k in ("build", "runbook", "ops", "source", "cargo")):
p = prefer("/overview/")
if p: return p
for s in ("/systems/", "/features/", "/overview/", "/reference/", "/entrypoints/"):
p = prefer(s)
if p: return p
return cands[0]
def gfm_slug(heading: str) -> str:
s = heading.strip().lower()
s = re.sub(r"[^\w\s-]", "", s, flags=re.UNICODE)
s = re.sub(r"\s+", "-", s)
s = re.sub(r"-+", "-", s).strip("-")
return s
# heading map for anchor fix
slug_counts = {}
heading_by_text = {}
for line in text.splitlines():
m = re.match(r"^(#{1,6})\s+(.+)$", line)
if not m:
continue
raw = m.group(2).strip()
base = gfm_slug(raw)
n = slug_counts.get(base, 0)
slug_counts[base] = n + 1
slug = base if n == 0 else f"{base}-{n}"
heading_by_text[raw] = slug
def repl(m):
label, href = m.group(1), m.group(2).strip()
# strip rustdoc
if "::" in href and not href.startswith(("http", "wiki/", "#", "../", "./")):
return f"`{label}`"
if href.startswith(("http://", "https://", "mailto:")):
return m.group(0)
if href.startswith("#"):
if label in heading_by_text and href[1:] != heading_by_text[label]:
return f"[{label}](#{heading_by_text[label]})"
g = gfm_slug(label)
# unique match on gfm of heading texts
for raw, slug in heading_by_text.items():
if gfm_slug(raw) == g and href[1:] != slug:
return f"[{label}](#{slug})"
return m.group(0)
path, sep, frag = href.partition("#")
frag_s = ("#" + frag) if frag else ""
if path.startswith("wiki/"):
return m.group(0)
m2 = re.match(r"^\.\./((?:systems|features|overview|entrypoints|reference)/.+)$", path)
if m2:
return f"[{label}](wiki/{m2.group(1)}{frag_s})"
if re.match(r"^(systems|features|overview|entrypoints|reference)/", path):
return f"[{label}](wiki/{path}{frag_s})"
if path.endswith(".md") and "/" not in path:
picked = pick(path, label)
if picked:
return f"[{label}]({picked}{frag_s})"
return m.group(0)
new = re.sub(r"\[([^\]]*)\]\(([^)]+)\)", repl, text)
out.write_text(new if new.endswith("\n") else new + "\n", encoding="utf-8")
PY
if [[ $DO_FIX -eq 1 ]]; then
mv "$tmp" "$src"
else
rm -f "$tmp"
fi
}
if [[ $DO_FIX -eq 1 ]]; then
fix_file "$FILE"
# Also fix .ai-context when draft mode
if [[ -n "$DRAFT" && -f "$DRAFT/.ai-context.md" ]]; then
fix_file "$DRAFT/.ai-context.md"
fi
fi
# Check
build_slugs "$FILE"
broken=0
total=0
while IFS= read -r line; do
# crude extract — enough for validation
:
done < /dev/null
# Python check for reliability
broken_out="$(python3 - "$FILE" "${DRAFT:-$(dirname "$FILE")}" "$WIKI" <<'PY'
import sys, re
from pathlib import Path
src = Path(sys.argv[1])
draft = Path(sys.argv[2])
wiki = Path(sys.argv[3])
text = src.read_text(encoding="utf-8", errors="replace")
def gfm_slug(heading: str) -> str:
s = heading.strip().lower()
s = re.sub(r"[^\w\s-]", "", s, flags=re.UNICODE)
s = re.sub(r"\s+", "-", s)
s = re.sub(r"-+", "-", s).strip("-")
return s
slug_counts = {}
slugs = set()
for line in text.splitlines():
m = re.match(r"^(#{1,6})\s+(.+)$", line)
if not m: continue
base = gfm_slug(m.group(2))
n = slug_counts.get(base, 0)
slug_counts[base] = n + 1
slugs.add(base if n == 0 else f"{base}-{n}")
broken = []
ok = 0
for m in re.finditer(r"\[([^\]]*)\]\(([^)]+)\)", text):
label, href = m.group(1), m.group(2).strip()
if href.startswith(("http://","https://","mailto:")):
ok += 1; continue
if href.startswith("#"):
if href[1:] in slugs:
ok += 1
else:
broken.append(f"anchor {href} ({label[:40]})")
continue
path = href.split("#",1)[0]
if not path:
ok += 1; continue
cands = [src.parent/path, draft/path, Path(path)]
if any(c.exists() for c in cands):
ok += 1
else:
broken.append(f"file {href} ({label[:40]})")
print(f"OK={ok}")
print(f"BROKEN={len(broken)}")
for b in broken[:50]:
print(b)
PY
)"
echo "$broken_out"
bcount="$(echo "$broken_out" | sed -n 's/^BROKEN=//p' | head -1)"
if [[ "${bcount:-1}" != "0" ]]; then
echo "okf-fix-links: FAILED ($bcount broken in $FILE)" >&2
exit 1
fi
echo "okf-fix-links: clean ($FILE)"
exit 0
+1
-1

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

"description": "Context-Driven Development: draft specs and plans before implementation. Structured workflows for features and fixes.",
"version": "3.5.3",
"version": "3.6.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.5.3",
"version": "3.6.0",
"author": {

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

@@ -5,3 +5,3 @@ {

"description": "Context-Driven Development: draft specs and plans before implementation. Structured workflows for features and fixes.",
"version": "3.5.3",
"version": "3.6.0",
"skills": "./skills/",

@@ -8,0 +8,0 @@ "agents": "./core/agents/",

@@ -51,2 +51,5 @@ 'use strict';

graph: true,
// The actual install root (scope/CURSOR_HOME aware) — used for the
// plugin-root marker instead of re-deriving a default path.
pluginRoot: base,
// Runs after the file copies: register + enable the plugin in the shared

@@ -53,0 +56,0 @@ // Claude registry. On a dry run it computes the merges and writes nothing.

@@ -13,5 +13,9 @@ 'use strict';

// On Windows, npm global CLIs are .cmd shims that CreateProcess can't resolve
// without a shell; elsewhere a shell is unnecessary overhead.
const USE_SHELL = process.platform === 'win32';
function hasBinary(name) {
// ENOENT on the error means the binary is not on PATH.
const r = spawnSync(name, ['--version'], { stdio: 'ignore', timeout: CHECK_TIMEOUT_MS });
const r = spawnSync(name, ['--version'], { stdio: 'ignore', timeout: CHECK_TIMEOUT_MS, shell: USE_SHELL });
return !(r.error && r.error.code === 'ENOENT');

@@ -29,3 +33,3 @@ }

if (ctx.dryRun) return 0;
const r = spawnSync(act.cmd, act.args, { stdio: 'inherit', timeout: STEP_TIMEOUT_MS });
const r = spawnSync(act.cmd, act.args, { stdio: 'inherit', timeout: STEP_TIMEOUT_MS, shell: USE_SHELL });
if (r.error) {

@@ -128,3 +132,3 @@ if (r.error.code === 'ETIMEDOUT') {

if (!ctx.dryRun) {
const root = writePluginRootMarker(host.id);
const root = writePluginRootMarker(host.id, plan.pluginRoot);
if (root) log.note(`Recorded plugin path for graph tooling: ${root}`);

@@ -131,0 +135,0 @@ }

@@ -21,2 +21,5 @@ 'use strict';

ensureDir(path.dirname(dest));
// Mirror, don't merge: dests are fully draft-owned bundled dirs, and a
// merge-copy would keep files deleted by newer releases around forever.
fs.rmSync(dest, { recursive: true, force: true });
fs.cpSync(src, dest, { recursive: true });

@@ -23,0 +26,0 @@ }

@@ -17,5 +17,10 @@ 'use strict';

// Resolve the installed draft plugin root for a given host, or null if unknown.
function resolvePluginRoot(hostId) {
// hintRoot: the root the just-executed install plan actually wrote to — it
// already accounts for scope (--project) and env overrides (CURSOR_HOME) that
// the per-host defaults below cannot see.
function resolvePluginRoot(hostId, hintRoot) {
const home = os.homedir();
if (hintRoot && fs.existsSync(path.join(hintRoot, 'scripts', 'tools'))) return hintRoot;
if (hostId === 'claude-code') {

@@ -81,5 +86,5 @@ // 1. Claude Code's own registry holds the authoritative installPath.

// Write ~/.cache/draft/plugin-root for the host. Returns the path written, or null.
function writePluginRootMarker(hostId) {
function writePluginRootMarker(hostId, hintRoot) {
try {
const root = resolvePluginRoot(hostId);
const root = resolvePluginRoot(hostId, hintRoot);
if (!root) return null;

@@ -86,0 +91,0 @@ const dest = path.join(os.homedir(), '.cache', 'draft', 'plugin-root');

@@ -183,3 +183,3 @@ # Condensation Subroutine

```bash
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -186,0 +186,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -19,3 +19,3 @@ # Git Report Metadata

```bash
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -22,0 +22,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -95,6 +95,6 @@ # Graph Query Subroutine

For common query modes, prefer the deterministic wrappers that ship with the plugin. Resolve their location via the canonical tool resolver (see [tool-resolver.md](tool-resolver.md)) before invoking. Skills run with cwd = the user's project and `${CLAUDE_PLUGIN_ROOT}` is **not** exported into skill Bash, so a bare `scripts/tools/foo.sh` fails — establish `DRAFT_TOOLS` once before the first helper call, in the same Bash session as your tool calls (re-establish it if you split helper calls into a separate, later Bash block):
For common query modes, prefer the deterministic wrappers that ship with the plugin. Resolve their location via the canonical tool resolver (see [tool-resolver.md](tool-resolver.md)) before invoking. Skills run with cwd = the user's project and `${CLAUDE_PLUGIN_ROOT}` is **not** exported into skill Bash, so a bare `scripts/tools/git-metadata.sh` fails — establish `DRAFT_TOOLS` once before the first helper call, in the same Bash session as your tool calls (re-establish it if you split helper calls into a separate, later Bash block):
```bash
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -215,3 +215,3 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

```bash
scripts/tools/graph-callers.sh --repo . --symbol <name>
"$DRAFT_TOOLS/graph-callers.sh" --repo . --symbol <name>
```

@@ -224,4 +224,4 @@

```bash
scripts/tools/graph-impact.sh --repo . --file <path> # changed-file impact (working-tree diff)
scripts/tools/graph-impact.sh --repo . --symbol <name> # transitive callers of a function
"$DRAFT_TOOLS/graph-impact.sh" --repo . --file <path> # changed-file impact (working-tree diff)
"$DRAFT_TOOLS/graph-impact.sh" --repo . --symbol <name> # transitive callers of a function
```

@@ -234,3 +234,3 @@

```bash
scripts/tools/hotspot-rank.sh --repo . [--top N]
"$DRAFT_TOOLS/hotspot-rank.sh" --repo . [--top N]
```

@@ -243,3 +243,3 @@

```bash
scripts/tools/cycle-detect.sh --repo .
"$DRAFT_TOOLS/cycle-detect.sh" --repo .
```

@@ -254,3 +254,3 @@

```bash
scripts/tools/graph-arch.sh --repo . \
"$DRAFT_TOOLS/graph-arch.sh" --repo . \
| jq '{packages, node_labels, edge_types, routes, layers, boundaries}'

@@ -264,4 +264,4 @@ ```

```bash
scripts/tools/mermaid-from-graph.sh --repo . --diagram module-deps # co-change coupling
scripts/tools/mermaid-from-graph.sh --repo . --diagram proto-map # detected routes
"$DRAFT_TOOLS/mermaid-from-graph.sh" --repo . --diagram module-deps # co-change coupling
"$DRAFT_TOOLS/mermaid-from-graph.sh" --repo . --diagram proto-map # detected routes
```

@@ -274,3 +274,3 @@

```bash
scripts/tools/graph-snippet.sh --repo . --qualified <pkg.Mod.Class.method>
"$DRAFT_TOOLS/graph-snippet.sh" --repo . --qualified <pkg.Mod.Class.method>
```

@@ -283,3 +283,3 @@

```bash
scripts/tools/graph-search.sh --repo . --query "auth token refresh" [--limit N]
"$DRAFT_TOOLS/graph-search.sh" --repo . --query "auth token refresh" [--limit N]
```

@@ -292,4 +292,4 @@

```bash
scripts/tools/graph-tests.sh --repo . --symbol <name> # tests covering a symbol
scripts/tools/graph-tests.sh --repo . --untested # exported symbols with no TESTS edge
"$DRAFT_TOOLS/graph-tests.sh" --repo . --symbol <name> # tests covering a symbol
"$DRAFT_TOOLS/graph-tests.sh" --repo . --untested # exported symbols with no TESTS edge
```

@@ -302,3 +302,3 @@

```bash
scripts/tools/graph-deps.sh --repo . [--file PATH]
"$DRAFT_TOOLS/graph-deps.sh" --repo . [--file PATH]
```

@@ -311,3 +311,3 @@

```bash
scripts/tools/graph-hierarchy.sh --repo . [--symbol <Class> | --derived <Base>]
"$DRAFT_TOOLS/graph-hierarchy.sh" --repo . [--symbol <Class> | --derived <Base>]
```

@@ -320,4 +320,4 @@

```bash
scripts/tools/graph-errors.sh --repo . --symbol <name> # what it raises/throws
scripts/tools/graph-errors.sh --repo . --type <ErrType> # who raises/throws that type
"$DRAFT_TOOLS/graph-errors.sh" --repo . --symbol <name> # what it raises/throws
"$DRAFT_TOOLS/graph-errors.sh" --repo . --type <ErrType> # who raises/throws that type
```

@@ -330,3 +330,3 @@

```bash
scripts/tools/graph-risk.sh --repo . [--min-complexity N]
"$DRAFT_TOOLS/graph-risk.sh" --repo . [--min-complexity N]
```

@@ -339,4 +339,4 @@

```bash
scripts/tools/graph-query.sh --repo . --cypher 'MATCH (f)-[:WRITES]->(v) RETURN f.name, v.name LIMIT 50'
scripts/tools/graph-query.sh --repo . --tool get_graph_schema --json '{}'
"$DRAFT_TOOLS/graph-query.sh" --repo . --cypher 'MATCH (f)-[:WRITES]->(v) RETURN f.name, v.name LIMIT 50'
"$DRAFT_TOOLS/graph-query.sh" --repo . --tool get_graph_schema --json '{}'
```

@@ -349,3 +349,3 @@

```bash
scripts/tools/graph-snapshot.sh --repo .
"$DRAFT_TOOLS/graph-snapshot.sh" --repo .
```

@@ -369,3 +369,3 @@

```bash
ENGINE_INFO="$(scripts/tools/verify-graph-binary.sh --repo . --json 2>/dev/null || true)"
ENGINE_INFO="$("$DRAFT_TOOLS/verify-graph-binary.sh" --repo . --json 2>/dev/null || true)"
# {"status":"ok","engine_bin":"...","source":"managed|path|bundled:<arch>|override","arch":"..."}

@@ -383,3 +383,3 @@ ```

```bash
scripts/tools/graph-snapshot.sh --repo .
"$DRAFT_TOOLS/graph-snapshot.sh" --repo .
```

@@ -386,0 +386,0 @@

@@ -18,3 +18,3 @@ ---

(it is only set for hooks, MCP/LSP servers, and monitor commands), so a bare
`scripts/tools/foo.sh` or `${CLAUDE_PLUGIN_ROOT}/...` invocation silently fails.
`scripts/tools/git-metadata.sh` or `${CLAUDE_PLUGIN_ROOT}/...` invocation silently fails.

@@ -28,12 +28,16 @@ Every skill MUST resolve `DRAFT_TOOLS` and invoke helpers as `"$DRAFT_TOOLS/<tool>.sh"`.

1. `${DRAFT_PLUGIN_ROOT}/scripts/tools` — explicit override (testing / pinned installs)
2. `$(cat ~/.cache/draft/plugin-root)/scripts/tools` — install marker written by `draft install` (authoritative)
3. `${CLAUDE_PLUGIN_ROOT}/scripts/tools` — set in hook/MCP contexts; harmless to probe
4. `installed_plugins.json → installPath` for `draft@*` — Claude Code's own registry (needs `jq`)
5. `~/.claude/plugins/cache/*/draft/*/scripts/tools` — newest cache install (glob, `sort -V`)
6. `~/.claude/plugins/marketplaces/*draft*/scripts/tools` — marketplace clone
7. `~/.cursor/plugins/local/draft/scripts/tools` — Cursor local install
8. `$PWD/scripts/tools` — dev / dogfooding (running inside the draft repo itself)
2. `$PWD/scripts/tools` — dev / dogfooding when cwd IS the draft repo (guarded by
`resolve-tools.sh`'s own presence, so it can never misfire in a user project;
deliberately beats the install marker so a repo checkout always wins)
3. `$(cat ~/.cache/draft/plugin-root)/scripts/tools` — install marker written by `draft install` (authoritative for installs)
4. `${CLAUDE_PLUGIN_ROOT}/scripts/tools` — set in hook/MCP contexts; harmless to probe
5. `installed_plugins.json → installPath` for `draft@*` — Claude Code's own registry (needs `jq`)
6. `~/.claude/plugins/cache/*/draft/*/scripts/tools` — newest cache install (glob, `sort -V`)
7. `~/.claude/plugins/marketplaces/*draft*/scripts/tools` — marketplace clone
8. `~/.cursor/plugins/local/draft/scripts/tools` — Cursor local install
9. `$PWD/scripts/tools` — last-resort cwd fallback (unguarded)
The marker (step 2) is the fast, authoritative path; steps 5–6 are the glob fallback
that keeps resolution working on installs predating the marker (no reinstall required).
The marker (step 3) is the fast, authoritative path for installs; steps 6–7 are the
glob fallback that keeps resolution working on installs predating the marker (no
reinstall required).

@@ -49,3 +53,3 @@ ## Skill preamble (copy verbatim)

```bash
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -64,5 +68,6 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

The four-line inline preamble is self-contained and is the recommended form for
skills — it needs no marker file and no prior `source`. The full 8-step resolver
(adding the `${DRAFT_PLUGIN_ROOT}` override, `${CLAUDE_PLUGIN_ROOT}`, the jq-registry
lookup, and the Cursor path) is shipped as `scripts/tools/resolve-tools.sh` for tests
skills — it needs no marker file and no prior `source`. The full resolver
(adding the `${DRAFT_PLUGIN_ROOT}` override, the guarded dogfood short-circuit,
`${CLAUDE_PLUGIN_ROOT}`, the jq-registry lookup, and the Cursor path) is shipped
as `scripts/tools/resolve-tools.sh` for tests
and for callers that prefer a single source of truth:

@@ -69,0 +74,0 @@

@@ -114,3 +114,3 @@ ---

```bash
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -117,0 +117,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

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

@@ -5,0 +5,0 @@ "bin": {

@@ -29,3 +29,3 @@ #!/usr/bin/env bash

usage() { sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; }
usage() { sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; }

@@ -32,0 +32,0 @@ while [[ $# -gt 0 ]]; do

@@ -61,2 +61,72 @@ #!/usr/bin/env bash

# ─────────────────────────────────────────────────────────
# Skill metadata: one row per skill — "<name>|<display header>|<copilot trigger>"
# The name and header never contain "|"; the trigger may (e.g. "[file|pr N]"),
# so lookups split on the FIRST TWO pipes only. Coverage against SKILL_ORDER is
# enforced by tests/test-trigger-functions.sh.
# ─────────────────────────────────────────────────────────
SKILL_META=(
'draft|Draft Overview|"help" or "draft"'
'init|Init Command|"init draft", "build the code graph", or "draft init [refresh] [--graph-only] [--module-only]"'
'graph|Graph Command|"build graph", "refresh graph", or "draft graph [path]"'
'new-track|New Track Command|"new feature" or "draft new-track <description>"'
'decompose|Decompose Command|"break into modules" or "draft decompose"'
'implement|Implement Command|"implement" or "draft implement"'
'coverage|Coverage Command|"check coverage" or "draft coverage"'
'deploy-checklist|Deploy Checklist Command|"deploy checklist" or "draft deploy-checklist [track <id>]"'
'bughunt|Bug Hunt Command|"hunt bugs" or "draft bughunt [--track <id>]"'
'review|Review Command|"review code" or "draft review [--track <id>] [--full]"'
'upload|Upload Command|"upload for review" or "draft upload [track <id>]"'
'plan|Plan Router|"plan feature" or "draft plan <intent>" (new-track, decompose, adr, tech-debt, change)'
'ops|Ops Router|"ops deploy" or "draft ops <intent>" (deploy-checklist, incident, standup, status, revert)'
'docs|Docs Router|"write docs" or "draft docs <intent>" (documentation)'
'discover|Discover Router|"discover debug" or "draft discover <intent>" (debug, bughunt, reviews, coverage, learn, index, etc.)'
'jira|Jira Router|"jira preview", "jira create", or "jira review <ID>"'
'integrations|Integrations Router|"integrations", "integrations jira-preview", or "integrations jira-create"'
'quick-review|Quick Review Command|"quick review" or "draft quick-review [file|pr <number>]"'
'deep-review|Deep Review Command|"deep review" or "draft deep-review [module]"'
'testing-strategy|Testing Strategy Command|"test strategy" or "draft testing-strategy [track <id>|path]"'
'learn|Learn Command|"learn patterns" or "draft learn [promote|migrate|path]"'
'adr|ADR Command|"document decision" or "draft adr [title]"'
'debug|Debug Command|"debug bug" or "draft debug [description|track <id>]"'
'standup|Standup Command|"standup" or "draft standup [date|week|save]"'
'tech-debt|Tech Debt Command|"tech debt" or "draft tech-debt [path|track <id>]"'
'incident-response|Incident Response Command|"incident" or "draft incident-response [new|update|postmortem]"'
'documentation|Documentation Command|"write docs" or "draft documentation [readme|runbook|api|onboarding]"'
'status|Status Command|"status" or "draft status"'
'revert|Revert Command|"revert" or "draft revert"'
'change|Change Command|"handle change" or "draft change <description>"'
'tour|Tour Command|"tour" or "draft tour"'
'impact|Impact Command|"impact" or "draft impact"'
'assist-review|Assist Review Command|"assist review" or "draft assist-review"'
)
# get_skill_header <skill> — display header for integration sections.
get_skill_header() {
local skill="$1" row rest
for row in "${SKILL_META[@]}"; do
if [[ "${row%%|*}" == "$skill" ]]; then
rest="${row#*|}"
printf '%s\n' "${rest%%|*}"
return 0
fi
done
printf '%s\n' "$(echo "${skill:0:1}" | tr '[:lower:]' '[:upper:]')${skill:1} Command"
}
# get_copilot_trigger <skill> — natural-language trigger for the Copilot header.
get_copilot_trigger() {
local skill="$1" row rest
for row in "${SKILL_META[@]}"; do
if [[ "${row%%|*}" == "$skill" ]]; then
rest="${row#*|}"
printf '%s\n' "${rest#*|}"
return 0
fi
done
printf '"draft %s"\n' "$skill"
}
# ─────────────────────────────────────────────────────────
# Core reference files (inlined by Claude plugin at runtime)

@@ -95,3 +165,2 @@ # ─────────────────────────────────────────────────────────

"templates/architecture.md"
"templates/track-architecture.md"
"templates/jira.md"

@@ -207,2 +276,4 @@ "templates/product.md"

"okf-validate-all.sh"
"okf-emit-catalog.sh"
"okf-fix-links.sh"
)

@@ -270,2 +341,4 @@

# Validate body format: line 1 blank, line 2 starts with #, line 3 blank.
# $3 (optional): pre-extracted body — skips a second extract_body parse when
# the caller already has it.
validate_skill_body_format() {

@@ -276,3 +349,7 @@ local skill="$1"

local body_head line1 line2 line3
body_head=$(extract_body "$skill_file" | sed -n '1,3p' || true)
if (( $# >= 3 )); then
body_head=$(printf '%s\n' "$3" | sed -n '1,3p')
else
body_head=$(extract_body "$skill_file" | sed -n '1,3p' || true)
fi
line1=$(echo "$body_head" | sed -n '1p')

@@ -279,0 +356,0 @@ line2=$(echo "$body_head" | sed -n '2p')

@@ -31,5 +31,8 @@ #!/usr/bin/env bash

# Escape single quotes for embedding inside a Cypher single-quoted string literal.
# Escape for embedding inside a Cypher single-quoted string literal.
# Backslashes must be doubled BEFORE quotes are escaped, otherwise an input
# ending in `\` yields `\'` — an escaped quote that never closes the literal.
gq_escape() {
local s="$1"
s="${s//\\/\\\\}"
printf '%s' "${s//\'/\\\'}"

@@ -86,7 +89,9 @@ }

# gq_symbol_status <project> <sym_esc> <result_json> -> ok | no-edges | no-match
# gq_symbol_status <project> <sym_esc> <result_json>
# -> ok | no-edges | no-match | probe-failed
# Fail-loud disambiguation (Guardrail 4): an empty result is only a true negative
# ("no-edges") when the node actually exists; otherwise it is "no-match". An
# existence probe runs only when the primary result is empty (hot path stays one
# query).
# query). If the probe itself errors, that is NOT a true negative — report
# "probe-failed" so callers never mistake an engine failure for absence.
gq_symbol_status() {

@@ -98,4 +103,6 @@ local project="$1" sym="$2" result="$3"

local ex
ex="$(gq_run "$project" "$(gq_q_exists "$sym")" || true)"
if [[ -n "$ex" && "$(gq_rows_len "$ex")" -gt 0 ]]; then
if ! ex="$(gq_run "$project" "$(gq_q_exists "$sym")")"; then
printf 'probe-failed'; return
fi
if [[ "$(gq_rows_len "$ex")" -gt 0 ]]; then
printf 'no-edges'

@@ -102,0 +109,0 @@ else

@@ -116,2 +116,43 @@ #!/usr/bin/env bash

# No legacy fallbacks: the Aether `graph`/`graph-clang` binaries are retired.
# GitHub-flavored-markdown-ish heading id (shared by TOC, anchors, link fixers).
# Keeps underscores; strips other punctuation; collapses whitespace to '-'.
gfm_slug() {
local s="${1:-}"
# shellcheck disable=SC2001
s="$(printf '%s' "$s" | tr '[:upper:]' '[:lower:]')"
s="$(printf '%s' "$s" | sed -E 's/[^[:alnum:]_[:space:]-]+//g; s/[[:space:]]+/-/g; s/-+/-/g; s/^-+//; s/-+$//')"
printf '%s' "$s"
}
# Count entries in an x-grounded-paths frontmatter field.
# Supports both inline (`x-grounded-paths: ["a", "b"]`) and block lists:
# x-grounded-paths:
# - "a"
# - "b"
grounded_paths_count() {
local file="$1"
[[ -f "$file" ]] || { echo 0; return; }
awk '
NR==1 && /^---$/ { fm=1; next }
fm && /^---$/ { exit }
fm && /^x-grounded-paths:[[:space:]]*\[/ {
line=$0
sub(/^x-grounded-paths:[[:space:]]*\[/, "", line)
sub(/\].*$/, "", line)
gsub(/[[:space:]]/, "", line)
if (line=="") { print 0; exit }
n=split(line, a, ",")
print n
exit
}
fm && /^x-grounded-paths:[[:space:]]*$/ { collect=1; next }
fm && collect {
if ($0 ~ /^[[:space:]]*-[[:space:]]*/) { n++; next }
if ($0 ~ /^[A-Za-z0-9_-]+:/) { print n+0; exit }
}
END { if (collect) print n+0 }
' "$file"
}
find_memory_bin() {

@@ -161,10 +202,2 @@ local repo_abs="$1"

[[ -n "$self_repo" && -d "$self_repo" ]] && roots+=("$self_repo")
for bc in \
"$HOME/.cursor/plugins/local/draft/.draft-install-path" \
"$HOME/.claude/plugins/draft/.draft-install-path"; do
if [[ -f "$bc" ]]; then
local pr; pr="$(cat "$bc" 2>/dev/null || true)"
[[ -n "$pr" && -d "$pr" ]] && roots+=("$pr")
fi
done

@@ -182,2 +215,17 @@ for pr in "${roots[@]}"; do

# graph_bootstrap <repo-dir>
# Shared engine bootstrap for the graph-*.sh wrappers: resolves the repo path,
# locates the engine binary, checks jq, and ensures the index. Sets the globals
# REPO_ABS and PROJECT. Returns 1 on any failure so each wrapper routes it to
# its own unavailable-JSON shape (the shapes differ per tool).
graph_bootstrap() {
local repo="$1" self_repo
REPO_ABS="$(cd "$repo" 2>/dev/null && pwd)" || return 1
self_repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
find_memory_bin "$REPO_ABS" "$self_repo" || return 1
command -v jq >/dev/null 2>&1 || return 1
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || return 1
}
# Run a codebase-memory-mcp CLI tool. Echoes the JSON result (stdout); the engine's

@@ -184,0 +232,0 @@ # `level=...` log lines go to stderr and are discarded unless DRAFT_MEMORY_DEBUG is set.

@@ -35,3 +35,3 @@ #!/usr/bin/env bash

case "$1" in
--root) ROOT="$2"; shift 2;;
--root) ROOT="${2:?--root requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -38,0 +38,0 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

@@ -54,4 +54,4 @@ #!/usr/bin/env bash

--json) EMIT_JSON=1; shift ;;
--caps) CAPS_CONF="$2"; shift 2 ;;
--skills-dir) SKILLS_DIR="$2"; shift 2 ;;
--caps) CAPS_CONF="${2:?--caps requires a value}"; shift 2 ;;
--skills-dir) SKILLS_DIR="${2:?--skills-dir requires a value}"; shift 2 ;;
-*) printf 'Unknown flag: %s\n' "$1" >&2; usage ;;

@@ -58,0 +58,0 @@ *) printf 'Unexpected arg: %s\n' "$1" >&2; usage ;;

@@ -77,2 +77,25 @@ #!/usr/bin/env bash

# Numeric reader for metadata.json:hygiene_budget.* (read_json_str is string-only).
read_json_num() {
local file="$1" key="$2"
[[ -f "$file" ]] || return 0
grep -oE "\"$key\"[[:space:]]*:[[:space:]]*-?[0-9]+" "$file" 2>/dev/null \
| head -1 | grep -oE '\-?[0-9]+$' || true
}
# TBD budget for every top-level doc of one track; cap < 0 means unlimited.
check_tbd_budget() {
local track_dir="$1" rel_track="$2" cap="$3" kind="$4" detail_suffix="$5"
(( cap >= 0 )) || return 0
while IFS= read -r f; do
local rel_file="${f#"$track_dir/"}"
local count
count="$(grep -oE "$TBD_RE" "$f" 2>/dev/null | wc -l | tr -d ' ' || true)"
if (( count > cap )); then
record "$rel_track" "$kind" "$rel_file" "0" \
"$count TBD sentinel(s) $detail_suffix"
fi
done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
}
scan_one_track() {

@@ -142,27 +165,24 @@ local track_dir="$1"

# 4. TBD budget per-doc.
# 4. TBD budget per-doc. Caps come from metadata.json:hygiene_budget when
# present (draft_tbd_cap / ready_for_review_tbd_cap; -1 = unlimited),
# falling back to the historical defaults.
case "$meta_status" in
draft|archived) ;;
archived) ;;
draft)
local draft_cap
draft_cap="$(read_json_num "$meta" "draft_tbd_cap")"
[[ "$draft_cap" =~ ^-?[0-9]+$ ]] || draft_cap=-1
check_tbd_budget "$track_dir" "$rel_track" "$draft_cap" "tbd-over-cap" \
"(cap $draft_cap at status=$meta_status)"
;;
ready-for-review|in_progress)
local tbd_cap=3
while IFS= read -r f; do
local rel_file="${f#"$track_dir/"}"
local count
count="$(grep -oE "$TBD_RE" "$f" 2>/dev/null | wc -l | tr -d ' ')"
if (( count > tbd_cap )); then
record "$rel_track" "tbd-over-cap" "$rel_file" "0" \
"$count TBD sentinel(s) (cap $tbd_cap at status=$meta_status)"
fi
done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
local tbd_cap
tbd_cap="$(read_json_num "$meta" "ready_for_review_tbd_cap")"
[[ "$tbd_cap" =~ ^-?[0-9]+$ ]] || tbd_cap=3
check_tbd_budget "$track_dir" "$rel_track" "$tbd_cap" "tbd-over-cap" \
"(cap $tbd_cap at status=$meta_status)"
;;
completed)
while IFS= read -r f; do
local rel_file="${f#"$track_dir/"}"
local count
count="$(grep -oE "$TBD_RE" "$f" 2>/dev/null | wc -l | tr -d ' ')"
if (( count > 0 )); then
record "$rel_track" "tbd-in-completed" "$rel_file" "0" \
"$count TBD sentinel(s) at status=completed"
fi
done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
check_tbd_budget "$track_dir" "$rel_track" 0 "tbd-in-completed" \
"at status=completed"
;;

@@ -169,0 +189,0 @@ esac

@@ -44,3 +44,3 @@ #!/usr/bin/env bash

case "$1" in
--root) ROOT="$2"; shift 2;;
--root) ROOT="${2:?--root requires a value}"; shift 2;;
--json) FORMAT="json"; shift;;

@@ -47,0 +47,0 @@ --jsonl) FORMAT="jsonl"; shift;;

@@ -43,3 +43,3 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -55,13 +55,6 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() { echo '{"cycles":[],"source":"unavailable"}'; exit 2; }
find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
command -v jq >/dev/null 2>&1 || unavailable
graph_bootstrap "$REPO" || unavailable
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable
# 2- and 3-node CALLS cycles. Cypher lives in _graph_queries.sh (label-agnostic;

@@ -68,0 +61,0 @@ # the Phase 0 fix — code units are mostly :Method, and CALLS only connects

@@ -42,3 +42,3 @@ #!/usr/bin/env bash

case "$1" in
--root) ROOT="$2"; shift 2;;
--root) ROOT="${2:?--root requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -45,0 +45,0 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

@@ -40,4 +40,9 @@ #!/usr/bin/env bash

# Validate that a payload was provided
if [[ -z "${payload}" ]]; then
# Trim trailing whitespace/newlines so the closing-brace strip below matches
# even when the caller passes a payload with a stray trailing newline.
payload="${payload%"${payload##*[![:space:]]}"}"
# Validate that a payload was provided and looks like a JSON object — a
# payload not ending in '}' would corrupt the NDJSON file, so drop it.
if [[ -z "${payload}" || "${payload}" != *\} ]]; then
exit 0

@@ -44,0 +49,0 @@ fi

@@ -49,4 +49,4 @@ #!/usr/bin/env bash

case "$1" in
--state) STATE_FILE="$2"; shift 2;;
--root) ROOT="$2"; shift 2;;
--state) STATE_FILE="${2:?--state requires a value}"; shift 2;;
--root) ROOT="${2:?--root requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -53,0 +53,0 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

@@ -69,8 +69,8 @@ #!/usr/bin/env bash

--project-metadata) WRITE_PROJECT_METADATA=1; shift;;
--project) PROJECT="$2"; shift 2;;
--module) MODULE="$2"; shift 2;;
--track-id) TRACK_ID="$2"; shift 2;;
--generated-by) GENERATED_BY="$2"; shift 2;;
--base) BASE_BRANCH="$2"; shift 2;;
--output-dir) OUTPUT_DIR="$2"; shift 2;;
--project) PROJECT="${2:?--project requires a value}"; shift 2;;
--module) MODULE="${2:?--module requires a value}"; shift 2;;
--track-id) TRACK_ID="${2:?--track-id requires a value}"; shift 2;;
--generated-by) GENERATED_BY="${2:?--generated-by requires a value}"; shift 2;;
--base) BASE_BRANCH="${2:?--base requires a value}"; shift 2;;
--output-dir) OUTPUT_DIR="${2:?--output-dir requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -77,0 +77,0 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

@@ -45,3 +45,3 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -57,13 +57,6 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() { echo '{"source":"unavailable"}'; exit 2; }
find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
command -v jq >/dev/null 2>&1 || unavailable
graph_bootstrap "$REPO" || unavailable
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable
ARCH_JSON="$(memory_cli get_architecture "{\"project\":\"$PROJECT\",\"aspects\":[\"all\"]}" || true)"

@@ -73,3 +66,4 @@ [[ -n "$ARCH_JSON" ]] || unavailable

# Validate it parses and looks like an architecture object before emitting.
# Tag with source like every other graph-*.sh wrapper (universal contract).
echo "$ARCH_JSON" | jq -e '.total_nodes != null' >/dev/null 2>&1 || unavailable
echo "$ARCH_JSON" | jq '.'
echo "$ARCH_JSON" | jq '. + {source:"memory-graph"}'

@@ -57,4 +57,4 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--symbol) SYMBOL="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--symbol) SYMBOL="${2:?--symbol requires a value}"; shift 2;;
--transitive) TRANSITIVE=1; shift;;

@@ -73,5 +73,2 @@ --transitive=*) TRANSITIVE=1; DEPTH="${1#*=}"; shift;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() {

@@ -83,8 +80,4 @@ jq -n --arg s "$SYMBOL" '{symbol:$s, callers:[], status:"unavailable", source:"unavailable"}' 2>/dev/null \

find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
command -v jq >/dev/null 2>&1 || unavailable
graph_bootstrap "$REPO" || unavailable
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable
SYM_ESC="$(gq_escape "$SYMBOL")"

@@ -101,4 +94,3 @@

if [[ "$N" -gt 0 ]]; then STATUS="ok"; else
EX="$(gq_run "$PROJECT" "$(gq_q_exists "$SYM_ESC")" || true)"
if [[ -n "$EX" && "$(gq_rows_len "$EX")" -gt 0 ]]; then STATUS="no-edges"; else STATUS="no-match"; fi
STATUS="$(gq_symbol_status "$PROJECT" "$SYM_ESC" '{"rows":[]}')"
fi

@@ -105,0 +97,0 @@ echo "$RES" | jq --arg s "$SYMBOL" --arg st "$STATUS" '

@@ -46,4 +46,4 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--file) FILE="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--file) FILE="${2:?--file requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -56,13 +56,6 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() { echo '{"imports":[],"total":0,"source":"unavailable"}'; exit 2; }
find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
command -v jq >/dev/null 2>&1 || unavailable
graph_bootstrap "$REPO" || unavailable
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable
RES="$(gq_run "$PROJECT" "$(gq_q_imports)" || true)"

@@ -69,0 +62,0 @@ [[ -n "$RES" ]] || unavailable

@@ -45,5 +45,5 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--symbol) SYMBOL="$2"; shift 2;;
--type) TYPE="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--symbol) SYMBOL="${2:?--symbol requires a value}"; shift 2;;
--type) TYPE="${2:?--type requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -60,5 +60,2 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() {

@@ -75,8 +72,4 @@ if [[ -n "$TYPE" ]]; then

find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
command -v jq >/dev/null 2>&1 || unavailable
graph_bootstrap "$REPO" || unavailable
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable
if [[ -n "$TYPE" ]]; then

@@ -83,0 +76,0 @@ SYM_ESC="$(gq_escape "$TYPE")"

@@ -46,5 +46,5 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--symbol) SYMBOL="$2"; shift 2;;
--derived) DERIVED="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--symbol) SYMBOL="${2:?--symbol requires a value}"; shift 2;;
--derived) DERIVED="${2:?--derived requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -60,13 +60,6 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() { echo '{"edges":[],"status":"unavailable","source":"unavailable"}'; exit 2; }
find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
command -v jq >/dev/null 2>&1 || unavailable
graph_bootstrap "$REPO" || unavailable
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable
if [[ -n "$SYMBOL" ]]; then

@@ -73,0 +66,0 @@ SYM_ESC="$(gq_escape "$SYMBOL")"; Q="$(gq_q_inherits_sym "$SYM_ESC")"; PROBE="$SYM_ESC"

@@ -46,6 +46,6 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--file) FILE="$2"; shift 2;;
--symbol) SYMBOL="$2"; shift 2;;
--depth) DEPTH="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--file) FILE="${2:?--file requires a value}"; shift 2;;
--symbol) SYMBOL="${2:?--symbol requires a value}"; shift 2;;
--depth) DEPTH="${2:?--depth requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -60,5 +60,2 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() {

@@ -73,13 +70,14 @@ local t="$1" k="$2"

find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable "$TARGET" "$KIND"
command -v jq >/dev/null 2>&1 || unavailable "$TARGET" "$KIND"
graph_bootstrap "$REPO" || unavailable "$TARGET" "$KIND"
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable "$TARGET" "$KIND"
if [[ -n "$SYMBOL" ]]; then
# direction:"both" is the reliable form (the "callers" value returns empty in this engine);
# we read the .callers array from it.
RES="$(memory_cli trace_path "{\"project\":\"$PROJECT\",\"function_name\":\"$SYMBOL\",\"depth\":$DEPTH,\"direction\":\"both\"}" || echo '{}')"
echo "${RES:-{\}}" | jq --arg t "$TARGET" '
# we read the .callers array from it. Payload built with jq so a quote in
# --symbol can never corrupt the JSON; an engine failure is unavailable,
# never a fabricated empty-impact success.
PAYLOAD="$(jq -n --arg p "$PROJECT" --arg f "$SYMBOL" --argjson d "$DEPTH" \
'{project:$p, function_name:$f, depth:$d, direction:"both"}')"
RES="$(memory_cli trace_path "$PAYLOAD" 2>/dev/null || true)"
echo "$RES" | jq -e . >/dev/null 2>&1 || unavailable "$TARGET" "$KIND"
echo "$RES" | jq --arg t "$TARGET" '
{target:$t, kind:"symbol",

@@ -90,4 +88,6 @@ impacted: [ (.callers // [])[] | {name:.name, file:(.qualified_name // ""), hop:(.hop // 1)} ],

# File impact: detect_changes maps the working-tree diff to impacted symbols.
RES="$(memory_cli detect_changes "{\"project\":\"$PROJECT\"}" || echo '{}')"
echo "${RES:-{\}}" | jq --arg t "$TARGET" '
PAYLOAD="$(jq -n --arg p "$PROJECT" '{project:$p}')"
RES="$(memory_cli detect_changes "$PAYLOAD" 2>/dev/null || true)"
echo "$RES" | jq -e . >/dev/null 2>&1 || unavailable "$TARGET" "$KIND"
echo "$RES" | jq --arg t "$TARGET" '
{target:$t, kind:"file",

@@ -94,0 +94,0 @@ impacted: [ (.impacted_symbols // [])[]

@@ -55,3 +55,3 @@ #!/usr/bin/env bash

case "$1" in
--scope) SCOPE="$2"; shift 2;;
--scope) SCOPE="${2:?--scope requires a value}"; shift 2;;
--module-only) MODULE_ONLY=1; shift;;

@@ -58,0 +58,0 @@ --no-fetch) NO_FETCH=1; shift;;

@@ -173,3 +173,3 @@ #!/usr/bin/env bash

[[ -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
if [[ "$GIT_OK" -eq 1 && "${LIMIT:-}" =~ ^[0-9]+$ && "$TRACKED" -gt "$LIMIT" ]]; then
warn "Tracked files ($TRACKED) > auto_index_limit ($LIMIT) — confirm the explicit index isn't truncated near $LIMIT."

@@ -176,0 +176,0 @@ fi

@@ -62,6 +62,6 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--cypher) CYPHER="$2"; shift 2;;
--tool) TOOL="$2"; shift 2;;
--json) TOOL_JSON="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--cypher) CYPHER="${2:?--cypher requires a value}"; shift 2;;
--tool) TOOL="${2:?--tool requires a value}"; shift 2;;
--json) TOOL_JSON="${2:?--json requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -102,13 +102,6 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() { echo '{"source":"unavailable"}'; exit 2; }
find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
command -v jq >/dev/null 2>&1 || unavailable
graph_bootstrap "$REPO" || unavailable
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable
if [[ -n "$CYPHER" ]]; then

@@ -115,0 +108,0 @@ RES="$(gq_run "$PROJECT" "$CYPHER" || true)"

@@ -45,4 +45,4 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--min-complexity) MIN_COMPLEXITY="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--min-complexity) MIN_COMPLEXITY="${2:?--min-complexity requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -56,13 +56,6 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() { echo '{"risky":[],"total":0,"source":"unavailable"}'; exit 2; }
find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
command -v jq >/dev/null 2>&1 || unavailable
graph_bootstrap "$REPO" || unavailable
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable
RES="$(gq_run "$PROJECT" "$(gq_q_risk)" || true)"

@@ -69,0 +62,0 @@ [[ -n "$RES" ]] || unavailable

@@ -43,5 +43,5 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--query) QUERY="$2"; shift 2;;
--limit) LIMIT="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--query) QUERY="${2:?--query requires a value}"; shift 2;;
--limit) LIMIT="${2:?--limit requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -56,5 +56,2 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() {

@@ -66,8 +63,4 @@ jq -n --arg q "$QUERY" '{query:$q, results:[], source:"unavailable"}' 2>/dev/null \

find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
command -v jq >/dev/null 2>&1 || unavailable
graph_bootstrap "$REPO" || unavailable
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable
ARGS="$(jq -n --arg p "$PROJECT" --arg q "$QUERY" --argjson n "$LIMIT" \

@@ -74,0 +67,0 @@ '{project:$p, query:$q, limit:$n}')"

@@ -55,4 +55,4 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--out) OUT_DIR="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--out) OUT_DIR="${2:?--out requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -103,2 +103,7 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

# YAML double-quoted scalars: escape backslashes then quotes so an unusual
# project name or engine version string can never corrupt the marker.
PROJECT_Y="${PROJECT//\\/\\\\}"; PROJECT_Y="${PROJECT_Y//\"/\\\"}"
VER_Y="${VER//\\/\\\\}"; VER_Y="${VER_Y//\"/\\\"}"
cat > "$OUT/schema.yaml" <<EOF

@@ -111,4 +116,4 @@ # Draft graph gate marker — written by scripts/tools/graph-snapshot.sh

engine: codebase-memory-mcp
engine_version: "$VER"
project: "$PROJECT"
engine_version: "$VER_Y"
project: "$PROJECT_Y"
generated_at: "$(date -u +%Y-%m-%dT%H:%M:%SZ)"

@@ -115,0 +120,0 @@ indexed_nodes: $NODES

@@ -47,4 +47,4 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--qualified|--symbol) QNAME="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--qualified|--symbol) QNAME="${2:?--qualified|--symbol requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -58,5 +58,2 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() {

@@ -68,8 +65,4 @@ jq -n --arg q "$QNAME" '{qualified_name:$q, status:"unavailable", source:"unavailable"}' 2>/dev/null \

find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
command -v jq >/dev/null 2>&1 || unavailable
graph_bootstrap "$REPO" || unavailable
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable
ARGS="$(jq -n --arg p "$PROJECT" --arg q "$QNAME" '{project:$p, qualified_name:$q}')"

@@ -76,0 +69,0 @@ RES="$(memory_cli get_code_snippet "$ARGS" 2>/dev/null || true)"

@@ -51,4 +51,4 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--symbol) SYMBOL="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--symbol) SYMBOL="${2:?--symbol requires a value}"; shift 2;;
--untested) UNTESTED=1; shift;;

@@ -63,5 +63,2 @@ --help|-h) usage; exit 0;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() {

@@ -77,8 +74,4 @@ if [[ "$UNTESTED" -eq 1 ]]; then

find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
command -v jq >/dev/null 2>&1 || unavailable
graph_bootstrap "$REPO" || unavailable
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable
if [[ "$UNTESTED" -eq 1 ]]; then

@@ -85,0 +78,0 @@ EXP="$(gq_run "$PROJECT" "$(gq_q_exported)" || true)"

@@ -52,4 +52,4 @@ #!/usr/bin/env bash

ingest) ACTION="ingest"; shift;;
--repo) REPO="$2"; shift 2;;
--file) FILE="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--file) FILE="${2:?--file requires a value}"; shift 2;;
--experimental) EXPERIMENTAL=1; shift;;

@@ -67,15 +67,8 @@ --help|-h) usage; exit 0;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() { echo '{"source":"unavailable"}'; exit 2; }
find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
command -v jq >/dev/null 2>&1 || unavailable
graph_bootstrap "$REPO" || unavailable
jq -e . "$FILE" >/dev/null 2>&1 || { echo "ERROR: --file is not valid JSON" >&2; exit 1; }
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable
ARGS="$(jq -n --arg p "$PROJECT" --slurpfile t "$FILE" '{project:$p, traces:($t[0])}')"

@@ -82,0 +75,0 @@ RES="$(memory_cli ingest_traces "$ARGS" 2>/dev/null || true)"

@@ -50,4 +50,4 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--top) TOP="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--top) TOP="${2:?--top requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -65,13 +65,6 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
unavailable() { echo '{"hotspots":[],"source":"unavailable"}'; exit 2; }
find_memory_bin "$REPO_ABS" "$SELF_REPO" || unavailable
command -v jq >/dev/null 2>&1 || unavailable
graph_bootstrap "$REPO" || unavailable
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || unavailable
ARCH_JSON="$(memory_cli get_architecture "{\"project\":\"$PROJECT\",\"aspects\":[\"hotspots\"]}" || true)"

@@ -92,3 +85,3 @@ [[ -n "$ARCH_JSON" ]] || unavailable

jq -n --slurpfile arch "$TMP_ARCH" --slurpfile props "$TMP_PROPS" --argjson top "$TOP" '
(($props[0].rows) // []) as $prows
((($props[0].rows) // []) | map(select(.[0] != null))) as $prows
| (reduce $prows[] as $r ({};

@@ -95,0 +88,0 @@ .[$r[0]] = {c:((($r[1]) // "0") | tonumber? // 0),

@@ -48,4 +48,4 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--diagram) DIAGRAM="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--diagram) DIAGRAM="${2:?--diagram requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -61,5 +61,2 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

REPO_ABS="$(cd "$REPO" && pwd)"
SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
stub() {

@@ -76,8 +73,4 @@ cat <<'EOF'

find_memory_bin "$REPO_ABS" "$SELF_REPO" || stub
command -v jq >/dev/null 2>&1 || stub
graph_bootstrap "$REPO" || stub
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
[[ -n "$PROJECT" ]] || stub
# module-deps: real IMPORTS edges (the auto-derived dependency graph). Self-imports

@@ -84,0 +77,0 @@ # (src == dst) are dropped so the diagram is a true cross-file graph. Capped at 40

@@ -226,3 +226,5 @@ #!/usr/bin/env bash

(( BACKUP )) && cp "$path" "$path.bak"
local _tmp; _tmp="$(mktemp "${path}.XXXXXX")"; printf '%s' "$after" > "$_tmp" && mv -f "$_tmp" "$path"
# %s\n restores the EOF newline stripped by command substitution
# (same pattern as fix-whitespace.sh).
local _tmp; _tmp="$(mktemp "${path}.XXXXXX")"; printf '%s\n' "$after" > "$_tmp" && mv -f "$_tmp" "$path"
printf 'migrate: stripped ephemeral frontmatter from %s\n' "$path"

@@ -229,0 +231,0 @@ fi

@@ -60,8 +60,8 @@ #!/usr/bin/env bash

case "$1" in
--plan) PLAN="$2"; shift 2;;
--bundle) BUNDLE="$2"; shift 2;;
--min-stub-lines) MIN_STUB_LINES="$2"; shift 2;;
--plan) PLAN="${2:?--plan requires a value}"; shift 2;;
--bundle) BUNDLE="${2:?--bundle requires a value}"; shift 2;;
--min-stub-lines) MIN_STUB_LINES="${2:?--min-stub-lines requires a value}"; shift 2;;
--no-coverage-page) WRITE_PAGE=0; shift;;
--json) JSON=1; shift;;
--report) REPORT="$2"; shift 2;;
--report) REPORT="${2:?--report requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -68,0 +68,0 @@ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

@@ -76,9 +76,9 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2;;
--scope) SCOPE="$2"; shift 2;;
--manifest) MANIFEST="$2"; shift 2;;
--min-fan-in) MIN_FAN_IN="$2"; shift 2;;
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
--scope) SCOPE="${2:?--scope requires a value}"; shift 2;;
--manifest) MANIFEST="${2:?--manifest requires a value}"; shift 2;;
--min-fan-in) MIN_FAN_IN="${2:?--min-fan-in requires a value}"; shift 2;;
--defer-below-floor) DEFER_BELOW_FLOOR=1; shift;;
--allow-defer) ALLOW_DEFER+=("$2"); shift 2;;
--out) OUT="$2"; shift 2;;
--out) OUT="${2:?--out requires a value}"; shift 2;;
--json) JSON=1; shift;;

@@ -115,3 +115,23 @@ --help|-h) usage; exit 0;;

DEGRADED="false"
DISCOVERY_META=() # e.g. cargo, npm, go, graph, heuristic
# Graph package names that are almost always tokenizer/type noise, not modules.
is_noise_package_name() {
local n="$1"
case "$n" in
str|list|dict|int|bool|float|string|type|mod|Cargo|cargo|profile|int64|uint|byte|char|void|null|None|true|false|i32|i64|u32|u64|f32|f64|Option|Result|Error|Self|self)
return 0;;
esac
return 1
}
# True if concept_id already recorded.
has_concept_id() {
local want="$1" id
for id in "${E_ID[@]:-}"; do
[[ "$id" == "$want" ]] && return 0
done
return 1
}
add_concept() {

@@ -122,3 +142,8 @@ # name section type resource fan_in required reason

[[ -n "$stem" ]] || stem="component"
E_ID+=("$section/$stem.md")
local cid="$section/$stem.md"
# Dedupe by concept_id (cargo + graph may both see the same crate).
if has_concept_id "$cid"; then
return 0
fi
E_ID+=("$cid")
E_TYPE+=("$type")

@@ -147,3 +172,141 @@ E_RES+=("$resource")

# --- 2. Graph path ---
# --- 2a. Language-aware workspace inventories (Cargo / npm / Go) ---
# These produce crate/package-level concepts that graph .packages often collapse
# or mis-label (especially Rust monorepos). Prefer path-grounded resources.
plan_from_cargo_workspace() {
local cargo="$REPO/Cargo.toml"
[[ -f "$cargo" ]] || return 1
# Only treat as workspace if [workspace] present with members.
grep -qE '^\[workspace\]' "$cargo" || return 1
grep -qE 'members\s*=' "$cargo" || return 1
local members=()
# Extract quoted paths inside the members = [ ... ] array (possibly multi-line).
local in_members=0 line m
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ members[[:space:]]*=[[:space:]]*\[ ]]; then
in_members=1
fi
if [[ $in_members -eq 1 ]]; then
while [[ "$line" =~ \"([^\"]+)\" ]]; do
m="${BASH_REMATCH[1]}"
members+=("$m")
line="${line#*\"$m\"}"
done
[[ "$line" == *"]"* ]] && in_members=0
fi
done < "$cargo"
[[ ${#members[@]} -gt 0 ]] || return 1
local path name ct required reason type
local added=0
for path in "${members[@]}"; do
[[ -z "$path" ]] && continue
# Skip globs we can't expand cheaply without bash nullglob walk
if [[ "$path" == *"*"* ]]; then
local base="${path%%/\*}"
[[ -d "$REPO/$base" ]] || continue
while IFS= read -r ct; do
name="$(basename "$(dirname "$ct")")"
[[ -z "$name" ]] && continue
if is_deferred_name "$name"; then
required=false; reason="allow-defer match"
else
required=true; reason=""
fi
type=Module
# Entrypoint packaging root if only main.rs and no lib.rs (heuristic)
if [[ -f "$(dirname "$ct")/src/main.rs" && ! -f "$(dirname "$ct")/src/lib.rs" ]]; then
type=Module
fi
add_concept "$name" systems "$type" "$(dirname "$ct" | sed "s|^$REPO/||")" 0 "$required" "$reason"
added=$((added+1))
done < <(find "$REPO/$base" -mindepth 1 -maxdepth 3 -type f -name 'Cargo.toml' 2>/dev/null | sort)
continue
fi
[[ -f "$REPO/$path/Cargo.toml" || -f "$REPO/$path" ]] || continue
local res="$path"
[[ -f "$REPO/$path" && "$path" == */Cargo.toml ]] && res="$(dirname "$path")"
name="$(basename "$res")"
if is_deferred_name "$name"; then
required=false; reason="allow-defer match"
else
required=true; reason=""
fi
add_concept "$name" systems Module "$res" 0 "$required" "$reason"
added=$((added+1))
done
[[ $added -gt 0 ]] || return 1
DISCOVERY_META+=("cargo")
return 0
}
plan_from_npm_workspaces() {
local pkg="$REPO/package.json"
[[ -f "$pkg" ]] || return 1
command -v jq >/dev/null 2>&1 || return 1
# workspaces may be array or { packages: [] }
local names
names="$(jq -r '
if .workspaces|type=="array" then .workspaces[]
elif .workspaces.packages|type=="array" then .workspaces.packages[]
else empty end
' "$pkg" 2>/dev/null || true)"
[[ -n "$names" ]] || return 1
local path name required reason added=0
while IFS= read -r path; do
[[ -z "$path" || "$path" == *"*"* ]] && continue
name="$(basename "$path")"
if is_deferred_name "$name"; then
required=false; reason="allow-defer match"
else
required=true; reason=""
fi
add_concept "$name" systems Module "$path" 0 "$required" "$reason"
added=$((added+1))
done <<< "$names"
[[ $added -gt 0 ]] || return 1
DISCOVERY_META+=("npm")
return 0
}
plan_from_go_modules() {
# go.work use directives, else single go.mod at root, else first-level */go.mod
local added=0 name required reason path
if [[ -f "$REPO/go.work" ]]; then
while IFS= read -r path; do
path="$(printf '%s' "$path" | sed -E 's/^[[:space:]]*//')"
[[ -z "$path" || "$path" == //* ]] && continue
[[ -f "$REPO/$path/go.mod" ]] || continue
name="$(basename "$path")"
if is_deferred_name "$name"; then
required=false; reason="allow-defer match"
else
required=true; reason=""
fi
add_concept "$name" systems Module "$path" 0 "$required" "$reason"
added=$((added+1))
done < <(awk '/^use[[:space:]]*\(/,/^\)/ {if ($1!="use" && $1!="(" && $1!=")") print $1}
/^use[[:space:]]+\./ {print $2}' "$REPO/go.work" | tr -d '"')
fi
if [[ $added -eq 0 && -f "$REPO/go.mod" ]]; then
name="$(basename "$REPO")"
add_concept "$name" systems Module "." 0 true ""
added=1
fi
if [[ $added -eq 0 ]]; then
while IFS= read -r path; do
name="$(basename "$(dirname "$path")")"
add_concept "$name" systems Module "$(dirname "$path" | sed "s|^$REPO/||")" 0 true ""
added=$((added+1))
done < <(find "$REPO" -mindepth 2 -maxdepth 3 -type f -name 'go.mod' 2>/dev/null | sort | head -50)
fi
[[ $added -gt 0 ]] || return 1
DISCOVERY_META+=("go")
return 0
}
# --- 2b. Graph path ---
plan_from_graph() {

@@ -154,3 +317,5 @@ local arch; arch="$(scripts_graph_arch)" || return 1

SOURCE="graph"
local have_lang_inventory=0
[[ ${#DISCOVERY_META[@]} -gt 0 ]] && have_lang_inventory=1
local name fan_in type required reason

@@ -160,2 +325,14 @@ # Packages → systems/<pkg>.md

[[ -z "$name" ]] && continue
# Drop tokenizer noise when we already have a real inventory (cargo/npm/go).
if [[ $have_lang_inventory -eq 1 ]] && is_noise_package_name "$name"; then
continue
fi
# Even without inventory, skip pure noise names that do not map to a dir.
if is_noise_package_name "$name"; then
local mapped=0
for d in "$REPO/$name" "$REPO/crates/$name" "$REPO/src/$name" "$REPO/third_party/$name"; do
[[ -d "$d" ]] && mapped=1 && break
done
[[ $mapped -eq 0 ]] && continue
fi
if is_deferred_name "$name"; then

@@ -166,9 +343,23 @@ required=false; reason="allow-defer match"; type=Module

elif [[ $DEFER_BELOW_FLOOR -eq 1 ]]; then
# Opt-in legacy behavior: low-fan-in packages are exempted.
required=false; reason="fan_in $fan_in < floor $MIN_FAN_IN"; type=Module
else
# Default: every package the graph knows about is documented. Fan-in
# below the floor only demotes Subsystem→Module; it never exempts.
required=true; reason=""; type=Module
fi
# When language inventory exists, graph packages that are coarse parents
# (e.g. codegen/) become Subsystems only if a matching directory exists.
if [[ $have_lang_inventory -eq 1 ]]; then
if [[ -d "$REPO/$name" || -d "$REPO/crates/$name" || -d "$REPO/third_party/$name" || -d "$REPO/prod/$name" ]]; then
type=Subsystem
# Prefer real path as resource when known
for d in "$name" "crates/$name" "third_party/$name" "prod/$name"; do
if [[ -d "$REPO/$d" ]]; then
add_concept "$name" systems "$type" "$d" "$fan_in" "$required" "$reason"
continue 2
fi
done
else
# Unmapped graph label with inventory present → skip (not a crate).
continue
fi
fi
add_concept "$name" systems "$type" "$name" "$fan_in" "$required" "$reason"

@@ -188,2 +379,4 @@ done < <(echo "$arch" | jq -r '.packages[]? | [.name, (.fan_in // 0)] | @tsv')

| sort -u)
DISCOVERY_META+=("graph")
return 0
}

@@ -229,9 +422,26 @@

# --- Drive discovery in priority order ---
# 1. --manifest (authoritative, exclusive)
# 2. Language inventories (Cargo / npm / Go) — can combine with graph parents
# 3. Graph packages (filtered when inventory present)
# 4. Heuristic top-level dirs
if [[ -n "$MANIFEST" ]]; then
[[ -f "$MANIFEST" ]] || { echo "ERROR: --manifest not found: $MANIFEST" >&2; exit 1; }
plan_from_manifest
elif plan_from_graph; then
:
DISCOVERY_META+=("manifest")
else
plan_from_heuristic
# Language inventories first so cargo crate names win concept_ids.
plan_from_cargo_workspace || true
plan_from_npm_workspaces || true
plan_from_go_modules || true
if plan_from_graph; then
:
elif [[ ${#E_ID[@]} -eq 0 ]]; then
plan_from_heuristic
DISCOVERY_META+=("heuristic")
fi
# If graph failed but we have cargo/npm/go, still success.
if [[ ${#E_ID[@]} -eq 0 ]]; then
plan_from_heuristic
DISCOVERY_META+=("heuristic")
fi
fi

@@ -244,2 +454,15 @@

# Compose SOURCE label from discovery meta
if [[ ${#DISCOVERY_META[@]} -gt 0 ]]; then
# de-dupe meta labels
SOURCE="$(printf '%s\n' "${DISCOVERY_META[@]}" | awk '!s[$0]++' | paste -sd+ -)"
[[ -z "$SOURCE" ]] && SOURCE="heuristic"
# degraded only when pure heuristic with no language/graph
if [[ "$SOURCE" == "heuristic" ]]; then
DEGRADED="true"
else
DEGRADED="false"
fi
fi
# Required-first, then deferred; stable within group (topological-ish: high fan-in

@@ -270,2 +493,10 @@ # subsystems first so forward cross-links resolve during generation).

printf ' "min_fan_in": %d,\n' "$MIN_FAN_IN"
printf ' "discovery": ['
local di first_d=1
for di in "${DISCOVERY_META[@]:-}"; do
[[ -z "$di" ]] && continue
[[ $first_d -eq 1 ]] && first_d=0 || printf ','
printf '"%s"' "$(json_escape "$di")"
done
printf '],\n'
# generated_order

@@ -272,0 +503,0 @@ printf ' "generated_order": ['

@@ -65,8 +65,8 @@ #!/usr/bin/env bash

case "$1" in
--arch-out) ARCH_OUT="$2"; shift 2;;
--arch-out) ARCH_OUT="${2:?--arch-out requires a value}"; shift 2;;
--concept-map-into) CMAP_INTO+=("$2"); shift 2;;
--section-indexes) SECTION_INDEXES=1; shift;;
--web) WEB_OUT="$2"; shift 2;;
--coverage-report) COVERAGE_REPORT="$2"; shift 2;;
--validated-at) VALIDATED_AT="$2"; shift 2;;
--web) WEB_OUT="${2:?--web requires a value}"; shift 2;;
--coverage-report) COVERAGE_REPORT="${2:?--coverage-report requires a value}"; shift 2;;
--validated-at) VALIDATED_AT="${2:?--validated-at requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -105,2 +105,34 @@ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

# Strip YAML frontmatter from a page (leading --- ... --- block on line 1).
# Rewrite markdown links so concatenated architecture.md (at draft/) resolves them.
# stdin body; $1 = bundle-relative path e.g. systems/foo.md
rewrite_body_links() {
local rel="${1:-}"
local sec="${rel%%/*}"
python3 -c '
import sys, re
sec = sys.argv[1]
text = sys.stdin.read()
sections = ("systems", "features", "overview", "entrypoints", "reference")
def repl(m):
label, href = m.group(1), m.group(2).strip()
if href.startswith(("http://", "https://", "mailto:", "#", "wiki/")):
return m.group(0)
if "::" in href and not href.startswith("../"):
return "`" + label + "`"
path, _, frag = href.partition("#")
frag_s = "#" + frag if frag else ""
if path.startswith("../"):
rest = path[3:]
if rest.split("/")[0] in sections:
return "[%s](wiki/%s%s)" % (label, rest, frag_s)
if any(path.startswith(s + "/") for s in sections):
return "[%s](wiki/%s%s)" % (label, path, frag_s)
if path.endswith(".md") and "/" not in path and sec in sections:
return "[%s](wiki/%s/%s%s)" % (label, sec, path, frag_s)
return m.group(0)
sys.stdout.write(re.sub(r"\[([^\]]*)\]\(([^)]+)\)", repl, text))
' "$sec"
}
strip_frontmatter() {

@@ -177,4 +209,3 @@ awk '

[[ -n "$title" ]] || title="$rel"
local anchor; anchor="$(printf '%s' "$title" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-')"
anchor="${anchor#-}"; anchor="${anchor%-}"
local anchor; anchor="$(gfm_slug "$title")"
echo " - [${title}](#${anchor})"

@@ -189,6 +220,12 @@ done < <(ordered_pages)

echo ""
strip_frontmatter "$BUNDLE/$rel"
strip_frontmatter "$BUNDLE/$rel" | rewrite_body_links "$rel"
done < <(ordered_pages)
} >"$tmp"
mv "$tmp" "$out"
if [[ -x "$SCRIPT_DIR/okf-fix-links.sh" ]]; then
draft_dir="$(cd "$(dirname "$out")" && pwd)"
if [[ -d "$draft_dir/wiki" || "$(basename "$BUNDLE")" == "wiki" ]]; then
"$SCRIPT_DIR/okf-fix-links.sh" --file "$out" --wiki "$BUNDLE" --fix >/dev/null 2>&1 || true
fi
fi
echo "rendered architecture view → $out ($(ordered_pages | grep -c . ) pages)"

@@ -394,3 +431,3 @@ }

blocks.push('<pre'+cls+'><code>'+esc(body.replace(/\n$/,''))+'</code></pre>');
return 'BLOCK'+(blocks.length-1)+'';
return 'BLOCK'+(blocks.length-1)+'';
});

@@ -418,3 +455,3 @@ var lines=src.split('\n'), out='', i=0, list='', tbl=[];

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+)$/);
var b=ln.match(/^BLOCK(\d+)$/);
if(b){ closeList(); out+=blocks[+b[1]]; continue; }

@@ -421,0 +458,0 @@ if(/^\s*$/.test(ln)){ closeList(); continue; }

@@ -9,5 +9,6 @@ #!/usr/bin/env bash

#
# Layer 1 okf-validate.sh structure (frontmatter, types, links, index)
# Layer 2 okf-validate-quality.sh per-type anti-stub / depth / mermaid lint
# Layer 3 okf-coverage-check.sh every required plan entry has a real page
# (may rewrite systems/coverage.md)
# Layer 1 okf-validate.sh structure LAST so coverage rewrite is checked
#

@@ -57,6 +58,6 @@ # Usage:

case "$1" in
--plan) PLAN="$2"; shift 2;;
--path-index) PATH_INDEX="$2"; shift 2;;
--plan) PLAN="${2:?--plan requires a value}"; shift 2;;
--path-index) PATH_INDEX="${2:?--path-index requires a value}"; shift 2;;
--strict) STRICT=1; shift;;
--report) REPORT="$2"; shift 2;;
--report) REPORT="${2:?--report requires a value}"; shift 2;;
--json) JSON=1; shift;;

@@ -84,12 +85,19 @@ --help|-h) usage; exit 0;;

# Layer 1: structure.
# Order matters:
# 1) quality — page content (does not rewrite files)
# 2) coverage — may REGENERATE systems/coverage.md (tool-owned)
# 3) structure — link/frontmatter check LAST so coverage rewrite cannot leave
# dangling links that structure already approved.
#
# Historical bug: structure-before-coverage let init promote a bundle whose
# coverage.md still had broken relative links (entrypoints/ from systems/).
v1_args=("$BUNDLE")
[[ -n "$PATH_INDEX" ]] && v1_args+=(--path-index "$PATH_INDEX")
if run_layer structure "$SCRIPT_DIR/okf-validate.sh" "${v1_args[@]}"; then L1=pass; else L1=fail; OVERALL=1; fi
q_args=("$BUNDLE"); [[ $STRICT -eq 1 ]] && q_args+=(--strict)
# Layer 2: quality.
q_args=("$BUNDLE"); [[ $STRICT -eq 1 ]] && q_args+=(--strict)
# Layer 2: quality (run first for fast fail on stubs).
if run_layer quality "$SCRIPT_DIR/okf-validate-quality.sh" "${q_args[@]}"; then L2=pass; else L2=fail; OVERALL=1; fi
# Layer 3: coverage (only if a plan is supplied).
# Layer 3: coverage (writes coverage.md when not --no-coverage-page).
if [[ -n "$PLAN" ]]; then

@@ -103,2 +111,5 @@ if run_layer coverage "$SCRIPT_DIR/okf-coverage-check.sh" --plan "$PLAN" --bundle "$BUNDLE"; then

# Layer 1: structure LAST (after coverage page is final).
if run_layer structure "$SCRIPT_DIR/okf-validate.sh" "${v1_args[@]}"; then L1=pass; else L1=fail; OVERALL=1; fi
REPORT_JSON="$(printf '{"valid":%s,"bundle":"%s","layers":{"structure":"%s","quality":"%s","coverage":"%s"}}\n' \

@@ -105,0 +116,0 @@ "$([[ $OVERALL -eq 0 ]] && echo true || echo false)" "$(json_escape "$BUNDLE")" "$L1" "$L2" "$L3")"

@@ -79,11 +79,5 @@ #!/usr/bin/env bash

# x-grounded-paths array length (entries inside [ ... ]).
# x-grounded-paths array length — inline `[a, b]` or YAML block list (via _lib.sh).
grounded_count() {
local arr
arr="$(grep -m1 -E '^x-grounded-paths:' "$1" 2>/dev/null || true)"
[[ -z "$arr" ]] && { echo 0; return; }
arr="${arr#*[}"; arr="${arr%]*}"
arr="$(printf '%s' "$arr" | tr -d ' ')"
[[ -z "$arr" ]] && { echo 0; return; }
awk -F',' '{print NF}' <<< "$arr"
grounded_paths_count "$1"
}

@@ -90,0 +84,0 @@

@@ -69,3 +69,3 @@ #!/usr/bin/env bash

case "$1" in
--path-index) PATH_INDEX="$2"; shift 2;;
--path-index) PATH_INDEX="${2:?--path-index requires a value}"; shift 2;;
--reverse) REVERSE=1; shift;;

@@ -241,2 +241,4 @@ --structure-only) STRUCTURE_ONLY=1; shift;;

# and ignore keys entirely. Each page must exist in the bundle.
# Flatten first: the index may be pretty-printed, so value arrays can
# span lines and a line-oriented match would silently skip them.
while IFS= read -r ref; do

@@ -247,3 +249,3 @@ [[ -z "$ref" ]] && continue

fi
done < <(grep -oE '\[[^]]*\]' "$PATH_INDEX" 2>/dev/null \
done < <(tr '\n' ' ' < "$PATH_INDEX" 2>/dev/null | grep -oE '\[[^]]*\]' \
| grep -oE '"[^"]+\.md"' | tr -d '"' | sort -u)

@@ -257,5 +259,6 @@ fi

if [[ $REVERSE -eq 1 && $STRUCTURE_ONLY -eq 0 && -n "$PATH_INDEX" && -f "$PATH_INDEX" ]]; then
# All pages the index maps to (its array values).
# All pages the index maps to (its array values). Flattened for the same
# pretty-printed-array reason as the forward check above.
INDEXED_FILE="$(mktemp)"
grep -oE '\[[^]]*\]' "$PATH_INDEX" 2>/dev/null \
tr '\n' ' ' < "$PATH_INDEX" 2>/dev/null | grep -oE '\[[^]]*\]' \
| grep -oE '"[^"]+\.md"' | tr -d '"' | sort -u > "$INDEXED_FILE" || true

@@ -262,0 +265,0 @@ while IFS= read -r -d '' page; do

@@ -52,6 +52,6 @@ #!/usr/bin/env bash

case "$1" in
--since) SINCE="$2"; shift 2;;
--limit) LIMIT="$2"; shift 2;;
--scope-pattern) SCOPE_PATTERN="$2"; shift 2;;
--branch) BRANCH="$2"; shift 2;;
--since) SINCE="${2:?--since requires a value}"; shift 2;;
--limit) LIMIT="${2:?--limit requires a value}"; shift 2;;
--scope-pattern) SCOPE_PATTERN="${2:?--scope-pattern requires a value}"; shift 2;;
--branch) BRANCH="${2:?--branch requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -58,0 +58,0 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

@@ -38,3 +38,3 @@ #!/usr/bin/env bash

case "$1" in
--root) ROOT="$2"; shift 2;;
--root) ROOT="${2:?--root requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -41,0 +41,0 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

@@ -51,3 +51,3 @@ #!/usr/bin/env bash

-h|--help) USAGE_HELP_MODE=1 usage ;;
--out) OUT_PATH="$2"; shift 2 ;;
--out) OUT_PATH="${2:?--out requires a value}"; shift 2 ;;
--stdout) TO_STDOUT=1; shift ;;

@@ -54,0 +54,0 @@ -*) printf 'Unknown flag: %s\n' "$1" >&2; usage ;;

@@ -54,3 +54,3 @@ #!/usr/bin/env bash

case "$1" in
--path) COVERAGE_PATH="$2"; shift 2;;
--path) COVERAGE_PATH="${2:?--path requires a value}"; shift 2;;
--schema-check) SCHEMA_CHECK="true"; shift;;

@@ -57,0 +57,0 @@ --help|-h) usage; exit 0;;

@@ -9,3 +9,3 @@ #!/usr/bin/env bash

# scripts/tools/scan-markers.sh [--root DIR] [--markers LIST]
# [--min-age-days N] [--include-untracked]
# [--min-age-days N]
#

@@ -42,5 +42,5 @@ # Exit codes: 0 OK (even with zero hits), 1 invocation error, 2 not a git repo

case "$1" in
--root) ROOT="$2"; shift 2;;
--markers) MARKERS="$2"; shift 2;;
--min-age-days) MIN_AGE="$2"; shift 2;;
--root) ROOT="${2:?--root requires a value}"; shift 2;;
--markers) MARKERS="${2:?--markers requires a value}"; shift 2;;
--min-age-days) MIN_AGE="${2:?--min-age-days requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -47,0 +47,0 @@ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

@@ -46,4 +46,4 @@ #!/usr/bin/env bash

case "$1" in
--require) REQUIRED="$2"; shift 2;;
--mode) MODE="$2"; shift 2;;
--require) REQUIRED="${2:?--require requires a value}"; shift 2;;
--mode) MODE="${2:?--mode requires a value}"; shift 2;;
--help|-h) usage; exit 0;;

@@ -50,0 +50,0 @@ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;

@@ -51,3 +51,3 @@ #!/usr/bin/env bash

--json) EMIT_JSON=1; shift ;;
--tolerance) TOLERANCE="$2"; shift 2 ;;
--tolerance) TOLERANCE="${2:?--tolerance requires a value}"; shift 2 ;;
-*) printf 'Unknown flag: %s\n' "$1" >&2; usage ;;

@@ -87,3 +87,3 @@ *) TRACK_PATHS+=("$1"); shift ;;

# lenient. The string form is portable across awk implementations.
while (match(s, "[A-Za-z0-9_][A-Za-z0-9_./-]*\\.[A-Za-z0-9]+:[0-9]+")) {
while (match(s, "[A-Za-z0-9_][A-Za-z0-9_./-]*\\.[A-Za-z0-9]+:[0-9]+(-[0-9]+)?")) {
cite = substr(s, RSTART, RLENGTH)

@@ -90,0 +90,0 @@ printf("%s\t%d\t%s\n", FILENAME, NR, cite)

@@ -153,3 +153,3 @@ #!/usr/bin/env bash

local paths
paths="$(echo "$line" | grep -oE '[A-Za-z][A-Za-z0-9_./-]*\.[A-Za-z]+' | sort -u)"
paths="$(echo "$line" | grep -oE '[A-Za-z][A-Za-z0-9_./-]*\.[A-Za-z]+' | sort -u || true)"
local path_count

@@ -156,0 +156,0 @@ path_count="$(printf '%s\n' "$paths" | grep -c . || true)"

@@ -54,4 +54,4 @@ #!/usr/bin/env bash

case "$1" in
--repo) REPO="$2"; shift 2 ;;
--plugin-root) PLUGIN_ROOT="$2"; shift 2 ;;
--repo) REPO="${2:?--repo requires a value}"; shift 2 ;;
--plugin-root) PLUGIN_ROOT="${2:?--plugin-root requires a value}"; shift 2 ;;
--json) EMIT_JSON=1; shift ;;

@@ -58,0 +58,0 @@ --verbose) VERBOSE=1; shift ;;

@@ -57,3 +57,3 @@ ---

```bash
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -60,0 +60,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -75,3 +75,3 @@ ---

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -78,0 +78,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -36,3 +36,3 @@ ---

```bash
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -79,3 +79,3 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

# Re-resolve helpers (this is a separate Bash session from Step 2).
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -82,0 +82,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -17,3 +17,3 @@ ---

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -20,0 +20,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -17,3 +17,3 @@ ---

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -421,3 +421,3 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -669,3 +669,3 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -672,0 +672,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -18,3 +18,3 @@ ---

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -334,3 +334,3 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -337,0 +337,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -70,3 +70,3 @@ ---

DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -73,0 +73,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -40,3 +40,3 @@ ---

# and call helpers as "$DRAFT_TOOLS/<tool>.sh". See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -43,0 +43,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -24,3 +24,3 @@ ---

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -27,0 +27,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -142,3 +142,3 @@ ---

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -643,3 +643,3 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

```bash
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -646,0 +646,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -283,3 +283,3 @@ ## architecture.md Specification (Supplementary Notes)

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -286,0 +286,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -136,71 +136,36 @@ ## Init — OKF Taxonomy Emitter (DRAFT_INIT_MODE=okf)

2. Plan → DETERMINISTIC. okf-plan-concepts.sh derives the expected-concept
set from the graph. EVERY package the graph knows about is
required (fan_in ≥ floor → Subsystem; below floor → Module — the
floor only types/orders, it never exempts); entrypoints → required;
only --allow-defer matches are deferred (with a reason). Writes
draft.tmp/.state/concept-plan.json.
set. Discovery order (exclusive of --manifest):
a) Cargo workspace members / npm workspaces / Go modules
b) graph packages (noise-filtered when a language inventory exists)
c) heuristic top-level source dirs
Coarse graph parents (e.g. crates/codegen) become Subsystems;
crate/package names become Modules. Tokenizer noise (str, int, …)
is dropped when unmapped. Writes draft.tmp/.state/concept-plan.json.
okf-plan-concepts.sh --repo . [--scope PATH] \
[--manifest FILE] [--min-fan-in 2] [--allow-defer GLOB]... \
--out draft.tmp/.state/concept-plan.json
This replaces the old in-context concept enumeration — the boundary
of the work is now a tool output, not an LLM judgment, so modules
and sub-modules cannot be silently dropped. (Legacy fan-in
exemption is opt-in via --defer-below-floor.) LOG the counts
(expected/required/deferred) BEFORE writing any page.
`generated_order` is topo-ish (required + high-fan-in first) so
forward cross-links resolve.
3. Generate → iterate concept-plan.generated_order; write ONE page per REQUIRED
entry, grounding each from the graph:
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.
Loop post-condition: every required concept_id has an output file.
⚠ Writing pages via shell heredoc without reading x-grounded-paths
sources, or finishing while any required entry is unwritten, is a
completeness failure — not a stylistic one.
4. Render views → ai-context.md (synopsis + Concept Map), architecture.md
(concatenated view + coverage banner), wiki/log.md (see M4).
5. Validate → the promotion gate. Run all layers via the orchestrator:
5a. okf-validate-all.sh draft.tmp/wiki \
--repo . \
--plan draft.tmp/.state/concept-plan.json \
--path-index draft.tmp/.state/path-to-concept.json \
--strict --report draft.tmp/.state/validation-report.json
It runs, in order: okf-validate.sh (structure + reverse index +
empty/untyped-page + leftover-template-token + dangling-link checks),
okf-validate-quality.sh (per-type anti-stub / depth / per-section
content / mermaid lint), okf-coverage-check.sh (every required plan
entry → real page).
ANY layer failing ⇒ exit non-zero ⇒ DO NOT atomic-rename.
coverage.md (systems/coverage.md) is regenerated by the coverage
layer; it is tool-owned (marker <!-- okf:coverage-generated -->) —
never hand-author it except deferral reasons in the manifest.
6. Emit → mv draft.tmp/ draft/ ONLY IF step 5 exit 0 ; update .state/.
On failure keep draft.tmp/ and surface validation-report.json.
LOG counts (expected/required/deferred + discovery[]) BEFORE pages.
3. Catalog floor → okf-emit-catalog.sh writes a quality-passing MINIMUM page for
every REQUIRED plan entry that is still missing (so XL monorepos
do not depend on the LLM for completeness):
okf-emit-catalog.sh --plan draft.tmp/.state/concept-plan.json \
--bundle draft.tmp/wiki --repo .
4. Generate/enrich → iterate concept-plan.generated_order; enrich top hotspots
(LLM) beyond the catalog floor. Record path-to-concept.json.
Post-condition: every required concept_id has a non-stub page.
5. Render views → okf-render-views.sh (architecture.md + Concept Map + section
indexes). Uses GFM slugs for TOC; rewrites sibling links to
wiki/<section>/…. Then okf-fix-links.sh --draft draft.tmp --fix.
6. Validate → promotion gate (order is intentional):
okf-validate-all.sh draft.tmp/wiki \
--plan draft.tmp/.state/concept-plan.json \
--path-index draft.tmp/.state/path-to-concept.json \
--strict --report draft.tmp/.state/validation-report.json
Runs: quality → coverage (rewrites coverage.md) → structure LAST
so coverage relative links are checked. Also run:
okf-fix-links.sh --draft draft.tmp --check
ANY failure ⇒ DO NOT atomic-rename.
7. Emit → mv draft.tmp/ draft/ ONLY IF step 6 exit 0 ; update .state/.
```
### Validation report schema (`.state/validation-report.json`)
```json
{ "valid": false, "bundle": "draft.tmp/wiki",
"layers": { "structure": "pass", "quality": "pass", "coverage": "fail" } }
```
### Component manifest (optional — `--manifest FILE`)
When the graph engine is unavailable (or a repo wants an authoritative list), pass
a plain-text manifest: one component name per line, `#` comments and blanks ignored.
Every listed component becomes a REQUIRED concept; `--allow-defer GLOB` still moves
matches to deferred. Without a manifest the plan comes from the graph, and only if
both are unavailable does it fall back to a heuristic top-level-dir scan (which it
marks `degraded: true`).
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)

@@ -259,13 +224,12 @@

```
1. Re-derive the plan: okf-plan-concepts.sh (modules added since last run become
REQUIRED — a new package can't slip through a refresh either)
2. Diff hashes.json vs working tree → changed source paths
3. path-to-concept.json → affected concept pages
4. Regenerate ONLY affected concepts; carry the rest verbatim (cached narration)
5. Re-render ai-context.md / architecture.md / log.md (cheap; always regenerated)
6. Re-validate (full gate): okf-validate-all.sh on the bundle with --plan and
--path-index. Refresh re-runs structure + quality + coverage — a changed
concept must still clear the quality bar, and a newly-required module must
still be present.
7. Append log.md; update hashes.json + path-to-concept.json
1. Re-derive the plan: okf-plan-concepts.sh (cargo/npm/go + graph). Modules added
since last run become REQUIRED — a new crate cannot slip through a refresh.
2. Diff plan vs previous concept-plan.json → NEW required concept_ids
3. Diff hashes.json vs working tree → changed source paths
4. path-to-concept.json → affected concept pages
5. okf-emit-catalog.sh for any NEW missing required pages (completeness floor)
6. Regenerate/enrich changed + new concepts; carry the rest verbatim
7. Re-render ai-context.md / architecture.md / log.md + okf-fix-links --fix
8. Re-validate: okf-validate-all + okf-fix-links --check
9. Append log.md; update hashes.json + path-to-concept.json + concept-plan.json
```

@@ -272,0 +236,0 @@

@@ -17,3 +17,3 @@ ---

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -20,0 +20,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -18,3 +18,3 @@ ---

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -21,0 +21,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -18,3 +18,3 @@ ---

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -388,3 +388,3 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -1134,3 +1134,3 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -1137,0 +1137,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -57,3 +57,3 @@ ---

```bash
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -60,0 +60,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -47,3 +47,3 @@ ---

```bash
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -50,0 +50,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -17,3 +17,3 @@ ---

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -99,3 +99,3 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

# is not exported into skill Bash). See core/shared/tool-resolver.md.
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -102,0 +102,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

@@ -55,3 +55,3 @@ ---

TRACK_DIR="draft/tracks/<id>"
DRAFT_TOOLS="$(cat ~/.cache/draft/plugin-root 2>/dev/null)/scripts/tools"
DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}/scripts/tools"
[ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/cache/*/draft/*/scripts/tools 2>/dev/null | sort -V | tail -1)"

@@ -58,0 +58,0 @@ [ -d "$DRAFT_TOOLS" ] || DRAFT_TOOLS="$(ls -d ~/.claude/plugins/marketplaces/*draft*/scripts/tools 2>/dev/null | tail -1)"

---
project: "{PROJECT_NAME}"
module: "root"
track_id: "{TRACK_ID}"
generated_by: "draft:decompose"
generated_at: "{ISO_TIMESTAMP}"
git:
branch: "{LOCAL_BRANCH}"
remote: "{REMOTE/BRANCH}"
commit: "{FULL_SHA}"
commit_short: "{SHORT_SHA}"
commit_date: "{COMMIT_DATE}"
commit_message: "{COMMIT_MESSAGE}"
dirty: false
synced_to_commit: "{FULL_SHA}"
---
# Track Architecture: {TRACK_TITLE}
> Track-scoped HLD/LLD for a single feature, bug fix, or refactor.
> Source of truth for implementation — `/draft:implement` consumes this to guide build order, contracts, and story generation.
> For project-wide architecture, see `draft/architecture.md`.
| Field | Value |
|-------|-------|
| **Track ID** | `{TRACK_ID}` |
| **Spec** | `./spec.md` |
| **Plan** | `./plan.md` |
| **Branch** | `{LOCAL_BRANCH}` → `{REMOTE/BRANCH}` |
| **Commit** | `{SHORT_SHA}` — {COMMIT_MESSAGE} |
| **Generated** | {ISO_TIMESTAMP} |
| **LLD Included** | {true | false} |
---
## Table of Contents
1. [Overview](#1-overview)
2. [Module Breakdown](#2-module-breakdown)
3. [High-Level Design (HLD)](#3-high-level-design-hld)
- 3.1 Component Diagram
- 3.2 Data Flow
- 3.3 Sequence Diagrams (Critical Flows)
- 3.4 State Machine(s)
4. [Dependency Analysis](#4-dependency-analysis)
5. [Implementation Order](#5-implementation-order)
6. [Low-Level Design (LLD)](#6-low-level-design-lld)
- 6.1 Per-Module API Contracts
- 6.2 Data Models & Schemas
- 6.3 Error Handling & Retry Semantics
- 6.4 Algorithm Pseudocode (where non-trivial)
7. [Notes & Decisions](#7-notes--decisions)
---
## 1. Overview
**What this track delivers:** {one paragraph from spec.md — the feature, bug fix, or refactor being scoped}
**Inputs:** {what triggers or feeds into this feature}
**Outputs:** {what this feature produces — data, side effects, API responses}
**Constraints:** {latency, throughput, compatibility, security — anything from spec.md Non-Functional Requirements}
**Integration points:** {which existing modules from `draft/.ai-context.md` this track touches}
---
## 2. Module Breakdown
### Modules Introduced or Modified
For each module in scope, fill out one block:
#### Module: `{module-name}`
- **Status:** `[ ] New` | `[ ] Modified` | `[x] Existing (unchanged)`
- **Responsibility:** {one sentence — what this module owns}
- **Files:** `{path/to/file1}`, `{path/to/file2}`
- **API Surface:** {public functions, classes, or interfaces — names only, contracts in §6.1}
- **Dependencies:** {other modules this imports from}
- **Complexity:** `Low` | `Medium` | `High`
- **Story placeholder:** _populated by `/draft:implement`_
{Repeat for each module.}
---
## 3. High-Level Design (HLD)
### 3.1 Component Diagram
Shows modules in scope + the external collaborators they talk to.
```mermaid
flowchart TD
subgraph Track["Track: {TRACK_ID}"]
M1["{module-1}"]
M2["{module-2}"]
M3["{module-3}"]
end
subgraph Existing["Existing System"]
E1["{existing-module-A}"]
E2["{existing-module-B}"]
end
subgraph External["External"]
X1["{DB / queue / API}"]
end
M1 --> M2
M2 --> M3
M1 --> E1
M3 --> X1
```
> Draw one node per module in scope. Include existing modules only when this track calls into them. Label edges with the transport (HTTP, RPC, queue, direct call) when non-obvious.
### 3.2 Data Flow
End-to-end flow of data through the track's modules.
```mermaid
flowchart LR
In["{input — request / event}"] --> V["{validation}"]
V --> L["{business logic}"]
L --> P["{persistence}"]
P --> Out["{output — response / emitted event}"]
```
> Replace with the actual transforms. If the track has distinct read and write paths, draw them separately.
### 3.3 Sequence Diagrams — Critical Flows
One sequence per acceptance criterion that involves more than a single module call. Skip for trivial single-module tracks.
#### Flow: {name — e.g., "Happy path: user submits X"}
```mermaid
sequenceDiagram
participant U as {Caller}
participant A as {module-1}
participant B as {module-2}
participant D as {DB / external}
U->>A: {request payload}
A->>B: {internal call}
B->>D: {query / write}
D-->>B: {result}
B-->>A: {response}
A-->>U: {final response}
Note over A,B: {invariant / gate — e.g., "tx must be open here"}
```
#### Flow: {error path — e.g., "Dependency timeout"}
```mermaid
sequenceDiagram
participant U as {Caller}
participant A as {module-1}
participant D as {External}
U->>A: {request}
A->>D: {call with timeout={N}ms}
D--xA: {timeout}
A->>A: {fallback / circuit breaker}
A-->>U: {degraded response or error}
```
### 3.4 State Machine(s)
Include only if the track introduces or modifies stateful entities. Omit otherwise.
```mermaid
stateDiagram-v2
[*] --> Pending
Pending --> Processing: start
Processing --> Complete: success
Processing --> Failed: error
Failed --> Pending: retry (max {N})
Failed --> DeadLetter: retries exhausted
Complete --> [*]
```
---
## 4. Dependency Analysis
### ASCII Dependency Graph
```
[module-1] ──> [module-2]
│ │
└──> [module-3] <──┘
```
### Dependency Table
| Module | Depends On | Depended By | Cycle? |
|--------|------------|-------------|--------|
| `{mod}` | `{list}` | `{list}` | no |
### Cycle Mitigation
_If any cycles detected, describe how they are broken (shared interface extraction, dependency inversion, etc.). Otherwise: "No cycles detected."_
---
## 5. Implementation Order
Topological sort — leaves first.
1. `{module-A}` (no internal deps) — foundational
2. `{module-B}` (depends on: A)
3. `{module-C}` (depends on: A, B)
**Parallel opportunities:** {which modules can be built concurrently}
---
## 6. Low-Level Design (LLD)
> Present when `--lld` flag was passed to `/draft:decompose` OR any module in §2 has `Complexity: High`. Otherwise this section reads: _"LLD not generated. Run `/draft:decompose --lld` to expand."_
### 6.1 Per-Module API Contracts
For each module in §2 marked `New` or `Modified`:
#### `{module-name}` — Public API
| Function / Method | Signature | Params | Returns | Errors / Exceptions |
|-------------------|-----------|--------|---------|---------------------|
| `{name}` | `{lang-appropriate signature}` | `{param: type — constraint}` | `{type — shape}` | `{error types / codes}` |
**Preconditions:** {what must be true before call — caller responsibilities}
**Postconditions:** {what is guaranteed after successful call}
**Invariants:** {properties preserved across calls — thread safety, idempotency, ordering}
{Repeat per module.}
### 6.2 Data Models & Schemas
Concrete shapes for every new or modified entity this track introduces.
#### `{ModelName}`
```{language}
{actual type definition — struct, class, interface, proto message, TypedDict, etc.}
```
| Field | Type | Nullable | Default | Validation / Constraint |
|-------|------|----------|---------|-------------------------|
| `{field}` | `{type}` | yes/no | `{default or —}` | `{rule}` |
**Storage:** {where persisted — table, collection, key prefix}
**Indexes / Keys:** {primary key, unique constraints, indexed fields}
**Migration:** {if this is a schema change — migration path and rollback}
{Repeat per model.}
### 6.3 Error Handling & Retry Semantics
Per-operation policy. One row per operation that has non-trivial error handling.
| Operation | Error Class | Classification | Retry? | Backoff | Max Attempts | Fallback |
|-----------|-------------|----------------|--------|---------|--------------|----------|
| `{op}` | `{ErrorType}` | transient / permanent / timeout | yes/no | `{policy}` | `{N}` | `{behavior}` |
**Propagation model:** {how errors surface — Result type, exceptions, error codes}
**Circuit breaker:** {thresholds, half-open policy, reset} — omit if N/A
**Idempotency:** {which operations are idempotent and how — dedup key, tx id}
### 6.4 Algorithm Pseudocode
Include only for non-trivial logic. Skip for straightforward CRUD.
#### {Algorithm name}
**Inputs:** `{...}`
**Outputs:** `{...}`
**Complexity:** `O({...})` time, `O({...})` space
```
{numbered or indented pseudocode — language-agnostic}
1. validate inputs
2. ...
3. return result
```
**Edge cases handled:**
- {case 1 — what happens}
- {case 2 — what happens}
---
## 7. Notes & Decisions
### Architecture Decisions
- {decision 1 — rationale, alternatives considered}
- {decision 2 — rationale, alternatives considered}
### Open Questions
- {question tracked during decomposition — to resolve before or during implementation}
### Links
- Spec: `./spec.md`
- Plan: `./plan.md`
- Related ADRs: `{paths if any, created via /draft:adr}`
- Project architecture: `draft/.ai-context.md` → `draft/architecture.md`

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