feat(brain): structured wing/hall taxonomy + obsidian-compatible layout
Adds a two-dimensional address (wing, hall) to brain notes. A wing is a
topic domain (e.g. jepa-fx, hyperguild); a hall is one of a closed
vocabulary of memory types (facts, decisions, failures, hypotheses,
sources). Notes route to brain/wiki/<wing>/<hall>/<slug>.md with
wing/hall/created_at YAML frontmatter, making the directory a valid
Obsidian vault.
Changes:
- new package ingestion/internal/brain (NotePath, ValidHalls, Sanitise,
BuildWingIndex, BuildAllWingIndexes)
- api.WriteNote refactored to WriteNoteOptions; wing+hall routes to
brain/wiki/, otherwise falls back to brain/knowledge/ (legacy)
- search.Query → QueryOptions with optional Wing/Hall filtering; Result
carries wing/hall extracted from frontmatter or path segments
- MCP tools brain_write and brain_query gain optional wing/hall params
(hall enum-validated); new brain_index tool regenerates _index.md MOC
- POST /index REST endpoint mirrors brain_index
- brain_write auto-rebuilds the wing's _index.md after a wing+hall write
- scripts/migrate-brain-halls.sh migrates flat brain/wiki/{concepts,entities}/
into the new layout (dry-run by default, --commit applies)
All existing tests pass; new tests cover wing/hall write routing, scope
filtering, invalid hall rejection, _index.md generation, and migration
script paths.
Closes hyperguild#1.
This commit is contained in:
135
scripts/migrate-brain-halls.sh
Executable file
135
scripts/migrate-brain-halls.sh
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
# migrate-brain-halls.sh — move flat brain/wiki/{concepts,entities}/ notes
|
||||
# into the structured brain/wiki/<wing>/<hall>/ layout introduced by
|
||||
# hyperguild#1.
|
||||
#
|
||||
# Reads each note's YAML frontmatter:
|
||||
# type: maps to hall (decision, hypothesis, failure, source → eponymous;
|
||||
# concept, entity, anything else → facts)
|
||||
# domain: maps to wing (sanitised: lowercase, alphanumerics + hyphens);
|
||||
# empty → "general"
|
||||
#
|
||||
# Dry-run by default. Pass --commit to actually move files. Idempotent:
|
||||
# already-migrated notes (already under a Wing dir) are left alone.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/migrate-brain-halls.sh /path/to/brain # dry-run
|
||||
# scripts/migrate-brain-halls.sh --commit /path/to/brain # apply
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
COMMIT=0
|
||||
BRAIN=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--commit) COMMIT=1 ;;
|
||||
-h|--help)
|
||||
sed -n '2,18p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*) BRAIN="$arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$BRAIN" ]]; then
|
||||
echo "error: brain directory required" >&2
|
||||
echo "usage: $0 [--commit] <brain-dir>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -d "$BRAIN" ]]; then
|
||||
echo "error: $BRAIN is not a directory" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
WIKI="$BRAIN/wiki"
|
||||
if [[ ! -d "$WIKI" ]]; then
|
||||
echo "no $WIKI/ — nothing to migrate"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
sanitise() {
|
||||
# lowercase, replace non-alnum with hyphen, collapse hyphens, trim
|
||||
local s
|
||||
s=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' \
|
||||
| sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
# extract_frontmatter_value <file> <key>
|
||||
# Echoes the value (trimmed, unquoted) of `key:` from a leading YAML
|
||||
# frontmatter block. Empty if absent or no frontmatter.
|
||||
extract_frontmatter_value() {
|
||||
awk -v key="$2" '
|
||||
BEGIN { in_fm = 0; first = 1 }
|
||||
/^---[[:space:]]*$/ {
|
||||
if (first) { in_fm = 1; first = 0; next }
|
||||
if (in_fm) { exit }
|
||||
}
|
||||
in_fm {
|
||||
idx = index($0, ":")
|
||||
if (idx == 0) next
|
||||
k = substr($0, 1, idx-1)
|
||||
v = substr($0, idx+1)
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", k)
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", v)
|
||||
gsub(/^["'\'']|["'\'']$/, "", v)
|
||||
if (k == key) { print v; exit }
|
||||
}
|
||||
' "$1"
|
||||
}
|
||||
|
||||
hall_for_type() {
|
||||
case "$1" in
|
||||
decision|decisions) echo "decisions" ;;
|
||||
hypothesis|hypotheses) echo "hypotheses" ;;
|
||||
failure|failures) echo "failures" ;;
|
||||
source|sources) echo "sources" ;;
|
||||
*) echo "facts" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
declare -i moved=0 skipped=0
|
||||
|
||||
migrate_source_dir() {
|
||||
local src="$1"
|
||||
[[ -d "$src" ]] || return 0
|
||||
while IFS= read -r -d '' f; do
|
||||
local typ domain wing hall slug dest
|
||||
typ=$(extract_frontmatter_value "$f" type)
|
||||
domain=$(extract_frontmatter_value "$f" domain)
|
||||
hall=$(hall_for_type "$typ")
|
||||
wing=$(sanitise "${domain:-general}")
|
||||
[[ -z "$wing" ]] && wing="general"
|
||||
slug=$(basename "$f" .md)
|
||||
dest="$WIKI/$wing/$hall/$slug.md"
|
||||
|
||||
if [[ "$f" == "$dest" ]]; then
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -e "$dest" ]]; then
|
||||
echo "skip (target exists): $f → $dest"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$COMMIT" -eq 1 ]]; then
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
git -C "$BRAIN" mv "$f" "$dest" 2>/dev/null || mv "$f" "$dest"
|
||||
fi
|
||||
echo "move: $f → $dest"
|
||||
moved=$((moved + 1))
|
||||
done < <(find "$src" -maxdepth 1 -type f -name '*.md' -print0)
|
||||
}
|
||||
|
||||
migrate_source_dir "$WIKI/concepts"
|
||||
migrate_source_dir "$WIKI/entities"
|
||||
|
||||
echo
|
||||
if [[ "$COMMIT" -eq 1 ]]; then
|
||||
echo "moved=$moved skipped=$skipped (committed)"
|
||||
else
|
||||
echo "moved=$moved skipped=$skipped (dry-run — pass --commit to apply)"
|
||||
fi
|
||||
Reference in New Issue
Block a user