add find-project-root + orient-contextdocs; route orient-project through find-project-root
commit 2ab2c888ab8e381ea83b93003b8dad08ac6536de · parent fd4110d019
· browse this tree
Ximianth, 2026-06-24 17:04:12 UTC
`find-project-root` is the single source of truth for "what is the current project". It's strict (git toplevel or nothing, exit 1 on miss); callers that want a softer fallback apply it themselves. Centralizing this means the inevitable future revisits to "what counts as a project" happen in one place, not five. `orient-contextdocs` walks from the project root *down* to a given path, surfacing conventional orientation docs (README/CONTRIBUTING/HACKME/AGENTS/ CLAUDE/.editorconfig) at each level along the way. Outermost-first ordering so the most-local context lands last. Markdown docs get a TOC (via `summarize-md-toc`); other docs just get listed by name. A `--skip-root` flag lets composers omit the project root when it's already covered separately (e.g. by `orient-project`). Silent when there's no project or no docs found. A few `# FUTURE:` markers note the planned integration point for path-keyed notes living in an associated meta repo. `orient-project` updated to use `find-project-root` rather than inlining its own resolution logic. Behaviour is unchanged (it still falls back to cwd when there's no project, since it should produce *some* orientation even outside one).
diff --git a/README.md b/README.md index 3e1b323..3547a7a 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ or compose into larger summaries (e.g. an agent's system prompt). ## Scripts +- `find-project-root` -- prints the project root path (git toplevel), or exits 1. +- `orient-contextdocs` -- finds orientation docs from the project root down to a given path. - `orient-cwd` -- describes the current working directory. - `orient-entrypoints` -- detects entrypoint systems (Makefile etc.) and lists their targets. - `orient-files` -- classifies files by type (extension, shebang, etc) and counts them. diff --git a/find-project-root b/find-project-root new file mode 100755 index 0000000..5f82d6d --- /dev/null +++ b/find-project-root @@ -0,0 +1,36 @@ +#!/bin/bash +# Single source of truth for "what is the current project root". +# +# Usage: find-project-root [PATH] +# PATH defaults to cwd. +# +# On success: prints the absolute path of the project root and exits 0. +# On failure: prints nothing and exits 1. +# +# Definition of "project root": +# - The top of the git work tree containing PATH. +# - If PATH is not inside any git work tree, there is no project root +# (and we report that by exiting 1 with no output). +# +# This is deliberately strict. Other compass scripts that want a softer +# fallback (e.g. "use cwd if there's no project") apply that fallback +# themselves -- it's a property of those scripts, not of the underlying +# notion of "project". +# +# FUTURE: when we add other project-marker conventions +# (e.g. a top-level `.project` file, or a configured marker name), +# this is the one place that resolves them. + +set -euo pipefail + +start="${1:-$PWD}" +if [ ! -d "$start" ]; then + echo "find-project-root: not a directory: $start" >&2 + exit 2 +fi + +if root=$(cd "$start" && git rev-parse --show-toplevel 2>/dev/null); then + echo "$root" + exit 0 +fi +exit 1 diff --git a/orient-contextdocs b/orient-contextdocs new file mode 100755 index 0000000..aab8a2f --- /dev/null +++ b/orient-contextdocs @@ -0,0 +1,150 @@ +#!/bin/bash +# Finds orientation docs along the path from the project root down to a given +# location, surfacing the context that applies to where you're working. +# +# Usage: orient-contextdocs [--skip-root] [PATH] +# PATH defaults to cwd. +# +# Walks from the project root (resolved via `find-project-root`) downward +# through each ancestor directory of PATH, all the way to PATH itself. +# At each level, collects conventional orientation docs: +# README, CONTRIBUTING, HACKME, AGENTS, CLAUDE (any case, common extensions), +# and `.editorconfig`. +# +# Markdown docs are summarized as a heading-only TOC (via `summarize-md-toc`) +# rather than inlined in full: context docs can be large, and the headings are +# usually enough to know whether to open them. +# +# Output ordering: outermost level first, so the most-local context lands +# last in the output -- where the reader's eye comes to rest. +# +# Silent (no output, exit 0) when: +# - PATH is not inside a project (no git work tree), or +# - no orientation docs are found at any level. +# +# Flags: +# --skip-root Omit docs at the project root itself. +# Useful when a composer is already inlining the root +# (e.g. via `orient-project`) and only wants the deeper +# ancestor context. +# Default (no flag) is to include the root: the script +# should make sense when run in isolation. +# +# FUTURE: an associated meta repo may also carry path-specific notes +# (e.g. `<meta>/notes-for-path/<repo-relative-path>/...`) so that private +# orientation can be maintained for specific subpaths of a public repo. +# The exact convention isn't pinned down yet, but the loop below is the +# natural integration point: at each per-directory step, also check the +# meta repo for a corresponding entry. + +set -euo pipefail + +self_dir=$(cd "$(dirname "$(readlink -f "$0")")" && pwd) +find_root="$self_dir/find-project-root" +summarize="$self_dir/summarize-md-toc" + +skip_root=0 +while [ $# -gt 0 ]; do + case "$1" in + --skip-root) skip_root=1; shift ;; + -h|--help) + sed -n '2,/^$/p' "$0" | sed 's/^# \?//' + exit 0 ;; + --) shift; break ;; + -*) echo "orient-contextdocs: unknown option: $1" >&2; exit 2 ;; + *) break ;; + esac +done + +start="${1:-$PWD}" +if [ ! -d "$start" ]; then + echo "orient-contextdocs: not a directory: $start" >&2 + exit 2 +fi +start=$(cd "$start" && pwd) + +# Resolve the project root. Silent exit if there isn't one. +if ! root=$("$find_root" "$start"); then + exit 0 +fi + +# Build the list of directories to inspect, from outermost (root) inward. +# We walk up from `start` collecting parents until we hit `root`, +# then reverse the order for outermost-first output. +levels=() +current="$start" +while :; do + levels=("$current" "${levels[@]}") + if [ "$current" = "$root" ]; then + break + fi + parent=$(dirname "$current") + if [ "$parent" = "$current" ]; then + # Defensive: shouldn't happen if find-project-root succeeded, + # but means `start` somehow isn't under `root`. + break + fi + current="$parent" +done + +if [ "$skip_root" -eq 1 ]; then + # Drop the first element (the root). + levels=("${levels[@]:1}") +fi + +# Per-level doc finder. +# Case-insensitive name match against the conventional prose files; +# `.editorconfig` matched exactly. +find_docs_in() { + local dir="$1" + local base candidate + shopt -s nullglob nocaseglob + for base in README CONTRIBUTING HACKME AGENTS CLAUDE; do + for candidate in "$dir/$base" "$dir/$base".md "$dir/$base".rst "$dir/$base".txt; do + if [ -f "$candidate" ]; then + echo "$candidate" + fi + done + done + shopt -u nocaseglob + if [ -f "$dir/.editorconfig" ]; then + echo "$dir/.editorconfig" + fi +} + +# Accumulate output; suppress everything if nothing was found. +buffer="" +for dir in "${levels[@]}"; do + mapfile -t docs < <(find_docs_in "$dir") + if [ ${#docs[@]} -eq 0 ]; then + continue + fi + + # Show paths relative to the project root for readability. + if [ "$dir" = "$root" ]; then + label="./" + else + label="${dir#$root/}/" + fi + buffer+=$'\n'"#### \`${label}\`"$'\n\n' + for doc in "${docs[@]}"; do + rel=$(basename "$doc") + buffer+="- \`${rel}\`"$'\n' + # Markdown: append the TOC nested under the bullet. + case "$doc" in + *.md|*.MD|*.Md|*.mD) + while IFS= read -r line; do + buffer+=" ${line}"$'\n' + done < <("$summarize" "$doc") + ;; + esac + + # FUTURE: meta-repo path-keyed notes integration point. + # Resolve the meta repo (when conventions are pinned down), + # look for notes keyed by repo-relative path, and inline/summarize. + done +done + +if [ -n "$buffer" ]; then + printf '%s' "$buffer" +fi diff --git a/orient-project b/orient-project index 93d7292..44f7060 100755 --- a/orient-project +++ b/orient-project @@ -15,10 +15,15 @@ set -euo pipefail # Resolve the project root. -# Explicit arg wins; otherwise prefer git toplevel; fall back to cwd. +# Delegated to `find-project-root` so the "what is a project" decision +# lives in exactly one place (`find-project-root` is strict: git toplevel +# or nothing). We add a cwd fallback here because this script wants to +# produce *some* orientation even outside a project context. +self_dir=$(cd "$(dirname "$(readlink -f "$0")")" && pwd) +find_root="$self_dir/find-project-root" if [ $# -ge 1 ]; then root=$(cd "$1" && pwd) -elif root=$(git rev-parse --show-toplevel 2>/dev/null); then +elif root=$("$find_root" 2>/dev/null); then : else root="$PWD"