@drafthq/draft
Advanced tools
| #!/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 |
@@ -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)" |
+1
-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 |
+79
-2
@@ -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 ' |