Files
agent-skills/llm-wiki/scripts/build-graph-data.sh

235 lines
7.0 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# build-graph-data.sh — build a path-identity graph and its sibling warning sidecar.
#
# Usage:
# bash scripts/build-graph-data.sh <kb-root> [<kb-internal-dir>/graph-data.json]
#
# A custom output must remain inside the knowledge base, keep the graph-data.json
# basename, and owns graph-warnings.json in the same existing directory.
# Exit codes: 0 = valid graph produced (including data warnings); 1 = tool/input failure.
set -eu
SCRIPT_DIR="${BASH_SOURCE[0]%/*}"
[ "$SCRIPT_DIR" = "${BASH_SOURCE[0]}" ] && SCRIPT_DIR="."
SCRIPT_DIR="$(cd "$SCRIPT_DIR" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/shared-config.sh"
usage() {
cat <<'USAGE'
Usage:
bash scripts/build-graph-data.sh <kb-root> [<kb-internal-dir>/graph-data.json]
Custom output rules:
- the destination directory must already exist inside the knowledge base
- the graph filename must remain graph-data.json
- graph-warnings.json is committed beside it as the unique paired warning file
USAGE
}
[ "$#" -ge 1 ] && [ "$#" -le 2 ] || {
usage >&2
exit 1
}
command -v jq >/dev/null 2>&1 || {
echo "ERROR: jq is not installed. Install it via:" >&2
print_install_hint jq
exit 1
}
command -v node >/dev/null 2>&1 || {
echo "ERROR: node is not installed. Install it via:" >&2
print_install_hint node
exit 1
}
WIKI_ROOT_INPUT="$1"
[ -d "$WIKI_ROOT_INPUT" ] || {
echo "ERROR: knowledge base does not exist or is not readable: $WIKI_ROOT_INPUT" >&2
exit 1
}
WIKI_ROOT="$(cd "$WIKI_ROOT_INPUT" && pwd)"
WIKI_DIR="$WIKI_ROOT/wiki"
[ -d "$WIKI_DIR" ] || {
echo "ERROR: wiki directory does not exist: $WIKI_DIR" >&2
echo " Run init-wiki.sh before building the graph." >&2
exit 1
}
if [ "$#" -eq 2 ]; then
OUTPUT_INPUT="$2"
OUTPUT_PARENT="$(dirname "$OUTPUT_INPUT")"
[ -d "$OUTPUT_PARENT" ] || {
echo "ERROR: custom graph output directory does not exist: $OUTPUT_PARENT" >&2
exit 1
}
OUTPUT="$(cd "$OUTPUT_PARENT" && pwd)/$(basename "$OUTPUT_INPUT")"
else
OUTPUT="$WIKI_ROOT/wiki/graph-data.json"
fi
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
HELPER="$SCRIPT_DIR/graph-analysis.js"
CLI="$SCRIPT_DIR/wiki-link-cli.js"
BUNDLE="$SCRIPT_DIR/lib/graph-warning-bundle.js"
[ -f "$HELPER" ] || {
echo "ERROR: 找不到图谱分析 helper:$HELPER" >&2
echo " Reinstall the skill and retry." >&2
exit 1
}
[ -f "$CLI" ] || {
echo "ERROR: shared wikilink resolver is missing: $CLI" >&2
echo " Reinstall the skill and retry." >&2
exit 1
}
[ -f "$BUNDLE" ] || {
echo "ERROR: graph warning bundle helper is missing: $BUNDLE" >&2
echo " Reinstall the skill and retry." >&2
exit 1
}
MAX_CONTENT_BYTES=$((2 * 1024 * 1024))
MAX_CONTENT_LINES=500
MAX_INSIGHT_NODES=250
MAX_INSIGHT_EDGES=1000
TMPDIR="$(mktemp -d -t llm-wiki-graph.XXXXXX)"
trap 'rm -rf "$TMPDIR"' EXIT
SCAN_DIR="$TMPDIR/scan"
mkdir -p "$SCAN_DIR"
GRAPH_FLAGS=""
if [ "${LLM_WIKI_TEST_MODE:-0}" = "1" ]; then
BUILD_DATE="2026-01-01T00:00:00Z"
GRAPH_FLAGS="--test-mode"
else
BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
fi
if [ -n "$GRAPH_FLAGS" ]; then
node "$CLI" graph "$WIKI_ROOT" "$SCAN_DIR" "$GRAPH_FLAGS" > "$TMPDIR/scan.out"
else
node "$CLI" graph "$WIKI_ROOT" "$SCAN_DIR" > "$TMPDIR/scan.out"
fi
for SCAN_FILE in nodes.json edges.json warning-groups.json candidate-sets.json scan-metrics.json; do
[ -f "$SCAN_DIR/$SCAN_FILE" ] || {
echo "ERROR: graph scan did not produce $SCAN_FILE" >&2
exit 1
}
done
WIKI_TITLE=""
if [ -f "$WIKI_ROOT/purpose.md" ]; then
WIKI_TITLE="$(awk '/^# / { sub(/^# +/, ""); print; exit }' "$WIKI_ROOT/purpose.md")"
fi
[ -n "$WIKI_TITLE" ] || WIKI_TITLE="$(basename "$WIKI_ROOT")"
TOTAL_SIZE="$(jq -r '.graph_source_bytes // 0' "$SCAN_DIR/scan-metrics.json")"
DEGRADE=0
if [ "$TOTAL_SIZE" -gt "$MAX_CONTENT_BYTES" ]; then
DEGRADE=1
fi
ANALYSIS_JSON="$TMPDIR/analysis.json"
if ! node "$HELPER" \
"$SCAN_DIR/nodes.json" \
"$SCAN_DIR/edges.json" \
"$ANALYSIS_JSON" \
"$DEGRADE" \
"$MAX_CONTENT_LINES" \
"$MAX_INSIGHT_NODES" \
"$MAX_INSIGHT_EDGES"; then
echo "ERROR: graph analysis helper failed: $HELPER" >&2
exit 1
fi
jq -e '
(.nodes | type) == "array" and
(.edges | type) == "array" and
(.insights | type) == "object" and
(.learning | type) == "object"
' "$ANALYSIS_JSON" > /dev/null 2>&1 || {
echo "ERROR: graph analysis helper returned invalid JSON: $ANALYSIS_JSON" >&2
exit 1
}
if [ "${LLM_WIKI_TEST_MODE:-0}" = "1" ]; then
jq '.nodes | sort_by(.id)' "$ANALYSIS_JSON" > "$TMPDIR/nodes.sorted.json"
jq '.edges | sort_by(.from, .to, .relation_type, .type)
| to_entries
| map(.value + {id: ("e" + ((.key + 1) | tostring))})' \
"$ANALYSIS_JSON" > "$TMPDIR/edges.sorted.json"
else
jq '.nodes' "$ANALYSIS_JSON" > "$TMPDIR/nodes.sorted.json"
jq '.edges' "$ANALYSIS_JSON" > "$TMPDIR/edges.sorted.json"
fi
INITIAL_VIEW="$TMPDIR/initial-view.json"
jq --slurpfile nodes "$TMPDIR/nodes.sorted.json" '
. as $edges
| ($nodes[0]) as $node_rows
| (reduce $edges[] as $edge ({};
.[$edge.from] = (.[$edge.from] // 0) + 1
| .[$edge.to] = (.[$edge.to] // 0) + 1
)) as $degree
| ($node_rows | group_by(.community // "_")) as $groups
| ([ $groups[] | max_by(($degree[.id] // 0)) | .id ]) as $representatives
| ($node_rows
| sort_by(-($degree[.id] // 0), .id)
| map(.id)
| map(select(. as $id | $representatives | index($id) | not))) as $rest
| ($representatives + $rest)[0:30]
' "$TMPDIR/edges.sorted.json" > "$INITIAL_VIEW"
NODE_COUNT="$(jq 'length' "$TMPDIR/nodes.sorted.json")"
EDGE_COUNT="$(jq 'length' "$TMPDIR/edges.sorted.json")"
INSIGHTS_DEGRADED="$(jq '.insights.meta.degraded == true' "$ANALYSIS_JSON")"
GRAPH_INPUT="$TMPDIR/graph-data.json"
jq -n \
--arg build_date "$BUILD_DATE" \
--arg wiki_title "$WIKI_TITLE" \
--argjson total_nodes "$NODE_COUNT" \
--argjson total_edges "$EDGE_COUNT" \
--slurpfile initial_view "$INITIAL_VIEW" \
--slurpfile nodes "$TMPDIR/nodes.sorted.json" \
--slurpfile edges "$TMPDIR/edges.sorted.json" \
--slurpfile analysis "$ANALYSIS_JSON" \
--argjson degraded "$DEGRADE" \
--argjson insights_degraded "$INSIGHTS_DEGRADED" \
'{
meta: {
build_date: $build_date,
wiki_title: $wiki_title,
total_nodes: $total_nodes,
total_edges: $total_edges,
initial_view: $initial_view[0],
degraded: ($degraded == 1),
insights_degraded: $insights_degraded
},
nodes: $nodes[0],
edges: $edges[0],
insights: $analysis[0].insights,
learning: $analysis[0].learning
}' > "$GRAPH_INPUT"
if ! node "$CLI" commit-pair \
"$WIKI_ROOT" \
"$GRAPH_INPUT" \
"$SCAN_DIR/warning-groups.json" \
"$SCAN_DIR/candidate-sets.json" \
"$OUTPUT" > "$TMPDIR/commit.out"; then
echo "ERROR: graph artifact pair commit failed for $OUTPUT" >&2
exit 1
fi
echo "Graph data generated: $OUTPUT"
echo " Nodes: $NODE_COUNT"
echo " Edges: $EDGE_COUNT"
echo " Warning sidecar: $(dirname "$OUTPUT")/graph-warnings.json"
[ "$DEGRADE" = "1" ] && echo " Warning: content embedding degraded above 2 MiB"
[ "$INSIGHTS_DEGRADED" = "true" ] && echo " Warning: insights degraded for graph size"
exit 0