From c81b1731aff9e2406b459de6717f2efa373bba6b Mon Sep 17 00:00:00 2001 From: bastien Date: Thu, 24 Sep 2026 12:12:16 +0200 Subject: [PATCH] feat(graphify): threshold signal from 200 tracked code files, the banner informs and the user decides lib/graphify-gate.sh counts tracked code files (graphify's AST extension set, vendored trees excluded) and, from 200 with no graphify-out/graph.json, prints one banner-sized line; session-start shows it with the /graphify hint. Nothing is built, installed or updated: the rule is the user's (BDR-097), grounded in the LRN-162 measurements (AST build 2.3 s, 0 tokens, a query 2 to 3k tokens). Doctrine section and plugin-advisor thresholds follow the same rule; graphify claude install stays rejected. Test: 11 checks. GRAPHIFY_MIN_CODE_FILES overrides the threshold. --- CHANGELOG.md | 8 +++++ CLAUDE.global.md | 6 ++-- agents/plugin-advisor.md | 4 +-- hooks/session-start.sh | 12 +++++++ lib/graphify-gate.sh | 41 ++++++++++++++++++++++ lib/tests/graphify-gate.test.sh | 60 +++++++++++++++++++++++++++++++++ 6 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 lib/graphify-gate.sh create mode 100644 lib/tests/graphify-gate.test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index aaad903..09f33d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] ### Added +- **graphify threshold signal** — `lib/graphify-gate.sh` counts tracked code + files (vendored trees excluded) and, from 200 with no + `graphify-out/graph.json`, the session-start banner shows one line + (`graphify? N code files ≥ 200, no graph`) plus the `/graphify` hint. It + informs, the user decides: nothing is built or installed. Doctrine and the + plugin-advisor thresholds follow the same rule; measured on a 295-file PHP + project: AST build 2.3 s, zero LLM tokens, one query 2 to 3k tokens. + Test `lib/tests/graphify-gate.test.sh` (11 checks). - **Branch deletion guard** — `gitflow_delete` (also `gitflow.sh delete `) is the only path that deletes a branch: it refuses `main` and `develop` (rc 6) and any branch not merged into develop or main (rc 5), diff --git a/CLAUDE.global.md b/CLAUDE.global.md index 41be2d2..2876673 100644 --- a/CLAUDE.global.md +++ b/CLAUDE.global.md @@ -343,8 +343,10 @@ generation, not micro-tweaks. ## graphify -ALL rules apply only if `graphify-out/graph.json` exists — else read files -directly. +Threshold: graphify from 200 tracked code files, never below (banner line +`graphify? N code files ≥ 200, no graph` informs, the user decides; never +build or `graphify claude install` without that go). ALL rules below apply +only if `graphify-out/graph.json` exists — else read files directly. - Codebase-wide question → `graphify query`; relationships → `path A B`; concept → `explain`. Scoped subgraph beats raw grep. - Known file / small task → read directly, no graphify. diff --git a/agents/plugin-advisor.md b/agents/plugin-advisor.md index 5388a5d..5ff0df1 100644 --- a/agents/plugin-advisor.md +++ b/agents/plugin-advisor.md @@ -79,9 +79,9 @@ Factors (weighted): **Score thresholds:** - **0-30% (simple)**: superpowers only. No gstack, no gsd, no ctx7, no graphify. _Examples: site vitrine, landing page, script CLI, simple CRUD._ -- **30-60% (moderate)**: + context7 if fast-libs, + graphify after implementation. +- **30-60% (moderate)**: + context7 if fast-libs. graphify only once the codebase passes 200 tracked code files (session-start banner informs, the user decides — BDR-097), never at scaffold. _Examples: blog with auth, dashboard with charts, API with validation._ -- **60-85% (complex)**: + gstack if browser-QA, + gsd if multi-session, + graphify both passes. +- **60-85% (complex)**: + gstack if browser-QA, + gsd if multi-session. graphify: same 200-file rule, likely reached — say so, do not pre-enable. _Examples: SaaS with billing, game with social features, e-commerce._ - **85-100% (enterprise)**: all tools justified. _Examples: multi-service platform, real-time collab app, marketplace._ diff --git a/hooks/session-start.sh b/hooks/session-start.sh index 029fafc..4bc1c2e 100644 --- a/hooks/session-start.sh +++ b/hooks/session-start.sh @@ -55,6 +55,14 @@ if [ -f "$_gf_lib" ] && git rev-parse --is-inside-work-tree >/dev/null 2>&1; the fi unset _gf_lib +# ── graphify threshold signal (BDR-097) ── +# Informs, never acts: one banner line when the repo holds ≥ 200 tracked code +# files and no graph. The user decides whether to build one. +GRAPHIFY_HINT="" +_gg_lib="$(dirname "${BASH_SOURCE[0]}")/../lib/graphify-gate.sh" +if [ -f "$_gg_lib" ]; then GRAPHIFY_HINT=$(bash "$_gg_lib" "$PWD" 2>/dev/null); fi +unset _gg_lib + # ── Toggle plugin detection ── TOGGLE_ACTIVE=() @@ -215,6 +223,10 @@ if [ -n "$GF_REFRESHED" ]; then printf "│ 🪝 %-44s│\n" "${_gf_line:0:44}" unset _gf_line fi +if [ -n "$GRAPHIFY_HINT" ]; then + printf "│ 🕸️ %-44s│\n" "${GRAPHIFY_HINT:0:44}" + printf "│ %-40s│\n" "→ /graphify (AST, seconds) — you decide" +fi # CLAUDE.global.md line-count guard (anti-regression). BDR-062 supersedes # BDR-031's 275 target: 305 is the assumed reality (extraction done at # job1; further compression costs clarity > token gain) — warn past 320. diff --git a/lib/graphify-gate.sh b/lib/graphify-gate.sh new file mode 100644 index 0000000..ba858f0 --- /dev/null +++ b/lib/graphify-gate.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# graphify-gate.sh — deterministic "propose graphify" signal (BDR-097). +# +# Rule (user, 2026-09-24): graphify only from 200 tracked code files. Below, +# grep + read is cheaper than a graph. The signal INFORMS, the user DECIDES: +# nothing here builds, installs or updates a graph. +# +# Sourced (functions) or executed: `graphify-gate.sh [dir]` prints one short +# line (banner-sized) and exits 0 when 's repo passes the threshold and has +# no graphify-out/graph.json; silent, rc 1 otherwise. GRAPHIFY_MIN_CODE_FILES +# overrides the threshold (tests). + +GRAPHIFY_MIN_CODE_FILES="${GRAPHIFY_MIN_CODE_FILES:-200}" +# Extensions graphify extracts by AST (tree-sitter): the proxy for "code file". +GRAPHIFY_CODE_EXT='py|js|mjs|cjs|ts|tsx|jsx|vue|svelte|astro|php|go|rs|java|kt|c|h|cpp|hpp|cc|cs|rb|swift|scala|sh|bash|lua|sql' +# Vendored trees sometimes committed; never the project's own code. +GRAPHIFY_VENDOR_DIRS='vendor|node_modules|third_party|dist|build' + +# graphify_code_file_count [dir] → tracked code files, vendored trees excluded. +# Tracked only (git ls-files): gitignored deps and build output never count. +graphify_code_file_count() { + git -C "${1:-.}" ls-files 2>/dev/null \ + | grep -v -E "(^|/)($GRAPHIFY_VENDOR_DIRS)/" \ + | grep -E -c "\.($GRAPHIFY_CODE_EXT)$" +} + +# graphify_gate [dir] → "graphify? N code files ≥ T, no graph" + rc 0 when the +# repo passes the threshold without a graph; silent rc 1 otherwise. +graphify_gate() { + local root n + root=$(git -C "${1:-.}" rev-parse --show-toplevel 2>/dev/null) || return 1 + [ -f "$root/graphify-out/graph.json" ] && return 1 # graph exists — nothing to propose + n=$(graphify_code_file_count "$root") + [ "$n" -ge "$GRAPHIFY_MIN_CODE_FILES" ] || return 1 + printf 'graphify? %s code files ≥ %s, no graph\n' "$n" "$GRAPHIFY_MIN_CODE_FILES" +} + +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + set -uo pipefail + graphify_gate "${1:-.}" +fi diff --git a/lib/tests/graphify-gate.test.sh b/lib/tests/graphify-gate.test.sh new file mode 100644 index 0000000..221d2fb --- /dev/null +++ b/lib/tests/graphify-gate.test.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# lib/tests/graphify-gate.test.sh — "propose graphify" threshold signal (BDR-097). +set -u +LIB="$(cd "$(dirname "$0")/../.." && pwd)/lib/graphify-gate.sh" +WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT +pass=0; fail=0 +check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1)); + printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; } +# gate(dir) -> the signal line, or "silent" +gate() { bash "$LIB" "$1" 2>/dev/null || echo silent; } +has() { case "$1" in *"$2"*) echo yes ;; *) echo no ;; esac; } +# mkrepo [] +mkrepo() { + local d="$WORK/$1" i + git init -q "$d"; git -C "$d" config user.email t@t; git -C "$d" config user.name t + git -C "$d" config core.hooksPath /dev/null + for i in $(seq 1 "$2"); do echo " "$d/f$i.php"; done + if [ "${3:-0}" -gt 0 ]; then + mkdir -p "$d/vendor/lib" + for i in $(seq 1 "$3"); do echo " "$d/vendor/lib/v$i.php"; done + fi + git -C "$d" add -A; git -C "$d" commit -q -m init + echo "$d" +} + +mkdir -p "$WORK/plain" +check T1-not-a-repo "$(gate "$WORK/plain")" silent + +d=$(mkrepo below 199) +check T2-199-files-silent "$(gate "$d")" silent + +d=$(mkrepo at 200) +check T3-200-files-fires "$(has "$(gate "$d")" "200 code files")" yes +check T3b-line-is-banner-sized "$([ "$(gate "$d" | wc -m)" -le 45 ] && echo yes || echo no)" yes +mkdir -p "$d/graphify-out"; echo '{}' > "$d/graphify-out/graph.json" +check T4-graph-exists-silent "$(gate "$d")" silent + +d=$(mkrepo vendored 190 60) +check T5-vendored-not-counted "$(gate "$d")" silent +for i in $(seq 191 200); do echo " "$d/f$i.php"; done +git -C "$d" add -A; git -C "$d" commit -q -m more +check T5b-own-files-reach-200 "$(has "$(gate "$d")" "200 code files")" yes + +d=$(mkrepo untracked 199) +for i in $(seq 200 210); do echo " "$d/f$i.php"; done # left untracked +check T6-untracked-not-counted "$(gate "$d")" silent + +d=$(mkrepo tiny 10) +check T7-threshold-override "$(has "$(GRAPHIFY_MIN_CODE_FILES=5 bash "$LIB" "$d" 2>/dev/null)" "≥ 5")" yes + +d=$(mkrepo subdir 200); mkdir -p "$d/app/sub" +check T8-from-subdirectory "$(has "$(gate "$d/app/sub")" "200 code files")" yes + +d=$(mkrepo docs 10) +for i in $(seq 1 300); do echo "# $i" > "$d/doc$i.md"; done +git -C "$d" add -A; git -C "$d" commit -q -m docs +check T9-non-code-not-counted "$(gate "$d")" silent + +echo "PASS=$pass FAIL=$fail" +[ "$fail" -eq 0 ]