@drafthq/draft 3.6.0 → 3.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +28 -49
- package/bin/README.md +47 -1
- package/cli/src/installer.js +1 -1
- package/cli/src/lib/cursor-registry.js +10 -0
- package/core/agents/architect.md +21 -9
- package/core/agents/debugger.md +5 -0
- package/core/agents/ops.md +7 -2
- package/core/agents/planner.md +7 -0
- package/core/agents/rca.md +14 -3
- package/core/agents/reviewer.md +1 -0
- package/core/agents/writer.md +6 -0
- package/core/knowledge-base.md +22 -3
- package/core/methodology.md +30 -5
- package/core/shared/condensation.md +27 -17
- package/core/shared/context-verify.md +3 -1
- package/core/shared/cross-skill-dispatch.md +7 -3
- package/core/shared/draft-context-loading.md +3 -0
- package/core/shared/git-report-metadata.md +1 -0
- package/core/shared/graph-query.md +0 -2
- package/core/shared/graph-usage-report.md +1 -1
- package/core/shared/jira-sync.md +10 -6
- package/core/shared/okf-retrieval.md +1 -1
- package/core/shared/parallel-analysis.md +14 -7
- package/core/shared/pattern-learning.md +2 -0
- package/core/shared/template-contract.md +1 -1
- package/core/shared/vcs-commands.md +5 -2
- package/core/templates/ai-context.md +3 -2
- package/core/templates/ai-profile.md +7 -0
- package/core/templates/architecture.md +6 -2
- package/core/templates/discovery.md +1 -1
- package/core/templates/guardrails.md +9 -0
- package/core/templates/hld.md +5 -4
- package/core/templates/intake-questions.md +45 -0
- package/core/templates/jira.md +11 -2
- package/core/templates/lld.md +8 -5
- package/core/templates/okf/concept.md +0 -1
- package/core/templates/product.md +11 -2
- package/core/templates/rca.md +6 -0
- package/core/templates/service-index.md +1 -0
- package/core/templates/spec.md +15 -0
- package/core/templates/tech-stack.md +12 -0
- package/core/templates/workflow.md +20 -0
- package/integrations/agents/AGENTS.md +1272 -292
- package/integrations/copilot/.github/copilot-instructions.md +1272 -292
- package/package.json +1 -1
- package/scripts/fetch-memory-engine.sh +24 -6
- package/scripts/lib.sh +11 -4
- package/scripts/tools/_graph_queries.sh +10 -3
- package/scripts/tools/_lib.sh +33 -11
- package/scripts/tools/check-repo-size.sh +125 -0
- package/scripts/tools/check-skill-line-caps.sh +1 -2
- package/scripts/tools/check-template-noop.sh +0 -1
- package/scripts/tools/classify-files.sh +2 -2
- package/scripts/tools/cycle-detect.sh +16 -8
- package/scripts/tools/fix-whitespace.sh +3 -0
- package/scripts/tools/git-metadata.sh +6 -0
- package/scripts/tools/graph-arch.sh +1 -1
- package/scripts/tools/graph-callers.sh +6 -3
- package/scripts/tools/graph-impact.sh +4 -2
- package/scripts/tools/graph-init.sh +12 -8
- package/scripts/tools/graph-preflight.sh +3 -0
- package/scripts/tools/graph-query.sh +70 -7
- package/scripts/tools/graph-snapshot.sh +29 -5
- package/scripts/tools/hotspot-rank.sh +24 -9
- package/scripts/tools/install-smoke-test.sh +223 -0
- package/scripts/tools/mermaid-from-graph.sh +28 -11
- package/scripts/tools/migrate-track-frontmatter.sh +9 -10
- package/scripts/tools/okf-coverage-check.sh +4 -3
- package/scripts/tools/okf-fix-links.sh +18 -93
- package/scripts/tools/okf-plan-concepts.sh +5 -8
- package/scripts/tools/okf-render-views.sh +19 -14
- package/scripts/tools/okf-validate-quality.sh +13 -2
- package/scripts/tools/okf-validate.sh +13 -2
- package/scripts/tools/parse-git-log.sh +19 -1
- package/scripts/tools/resolve-tools.sh +24 -8
- package/scripts/tools/verify-citations.sh +1 -2
- package/scripts/tools/verify-doc-anchors.sh +13 -33
- package/scripts/tools/verify-graph-binary.sh +13 -3
- package/skills/GRAPH.md +21 -10
- package/skills/adr/SKILL.md +24 -6
- package/skills/assist-review/SKILL.md +2 -1
- package/skills/bughunt/SKILL.md +54 -11
- package/skills/bughunt/references/regression-tests.md +14 -4
- package/skills/change/SKILL.md +19 -9
- package/skills/coverage/SKILL.md +26 -10
- package/skills/debug/SKILL.md +12 -2
- package/skills/decompose/SKILL.md +39 -9
- package/skills/deep-review/SKILL.md +17 -4
- package/skills/deploy-checklist/SKILL.md +9 -2
- package/skills/documentation/SKILL.md +12 -1
- package/skills/draft/SKILL.md +40 -29
- package/skills/draft/context-files.md +8 -1
- package/skills/draft/quality-guide.md +10 -2
- package/skills/graph/SKILL.md +1 -1
- package/skills/impact/SKILL.md +3 -1
- package/skills/implement/SKILL.md +60 -18
- package/skills/incident-response/SKILL.md +5 -2
- package/skills/init/SKILL.md +102 -23
- package/skills/init/references/architecture-spec.md +31 -7
- package/skills/init/references/okf-emitter.md +3 -3
- package/skills/jira/SKILL.md +33 -13
- package/skills/jira/references/review.md +61 -19
- package/skills/learn/SKILL.md +29 -6
- package/skills/new-track/SKILL.md +73 -18
- package/skills/quick-review/SKILL.md +7 -2
- package/skills/revert/SKILL.md +11 -5
- package/skills/review/SKILL.md +123 -24
- package/skills/review/references/zero-setup-mode.md +117 -0
- package/skills/standup/SKILL.md +9 -1
- package/skills/status/SKILL.md +6 -6
- package/skills/tech-debt/SKILL.md +7 -1
- package/skills/testing-strategy/SKILL.md +10 -2
- package/skills/tour/SKILL.md +2 -1
- package/skills/upload/SKILL.md +3 -2
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# which scripts/tools/_lib.sh:find_memory_bin resolves.
|
|
9
9
|
#
|
|
10
10
|
# Pinned by default for reproducibility; override with CMM_VERSION (a tag, e.g.
|
|
11
|
-
# "v0.
|
|
11
|
+
# "v0.9.0", or "latest").
|
|
12
12
|
#
|
|
13
13
|
# Usage:
|
|
14
14
|
# scripts/fetch-memory-engine.sh [--dest DIR] [--force]
|
|
@@ -16,12 +16,18 @@
|
|
|
16
16
|
# Env:
|
|
17
17
|
# CMM_VERSION Release tag to fetch (default: pinned DEFAULT_VERSION).
|
|
18
18
|
# CMM_DOWNLOAD_URL Override the release base URL (testing).
|
|
19
|
+
# DRAFT_STRICT_VERIFY 1 = refuse to install when the checksum cannot be
|
|
20
|
+
# verified (missing checksums.txt or unlisted archive).
|
|
21
|
+
# Default is warn-and-continue, which keeps installs working
|
|
22
|
+
# on releases that ship no checksums file — acceptable for
|
|
23
|
+
# individuals, not for anyone who must attest to what runs
|
|
24
|
+
# on their machine.
|
|
19
25
|
#
|
|
20
26
|
# Exit codes: 0 installed/already-present, 1 invocation error, 2 fetch/verify failure.
|
|
21
27
|
set -euo pipefail
|
|
22
28
|
|
|
23
29
|
REPO="DeusData/codebase-memory-mcp"
|
|
24
|
-
DEFAULT_VERSION="v0.
|
|
30
|
+
DEFAULT_VERSION="v0.9.0" # pinned; bump deliberately. NOTE: tag must carry the leading "v" AND have published assets (0.7.0 had none → 404).
|
|
25
31
|
VERSION="${CMM_VERSION:-$DEFAULT_VERSION}"
|
|
26
32
|
DEST="$HOME/.cache/draft/bin"
|
|
27
33
|
FORCE=0
|
|
@@ -79,7 +85,19 @@ if ! curl -fSL --max-time 300 -o "$TMP/$ARCHIVE" "$BASE/$ARCHIVE"; then
|
|
|
79
85
|
exit 2
|
|
80
86
|
fi
|
|
81
87
|
|
|
82
|
-
# --- Verify checksum
|
|
88
|
+
# --- Verify checksum ---
|
|
89
|
+
# A mismatch is always fatal. An *absent* checksum is fatal only under
|
|
90
|
+
# DRAFT_STRICT_VERIFY=1 — otherwise it warns, so that a release without a
|
|
91
|
+
# checksums.txt does not brick the install for everyone.
|
|
92
|
+
STRICT="${DRAFT_STRICT_VERIFY:-0}"
|
|
93
|
+
unverified() {
|
|
94
|
+
if [[ "$STRICT" == "1" ]]; then
|
|
95
|
+
echo "error: $1 (DRAFT_STRICT_VERIFY=1 refuses unverified binaries)" >&2
|
|
96
|
+
exit 2
|
|
97
|
+
fi
|
|
98
|
+
echo " warning: $1 — skipping verification (set DRAFT_STRICT_VERIFY=1 to make this fatal)" >&2
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
if curl -fsSL --max-time 60 -o "$TMP/checksums.txt" "$BASE/checksums.txt" 2>/dev/null; then
|
|
84
102
|
expected="$(grep " $ARCHIVE\$" "$TMP/checksums.txt" 2>/dev/null | awk '{print $1}' | head -1 || true)"
|
|
85
103
|
if [[ -n "$expected" ]]; then
|
|
@@ -92,12 +110,12 @@ if curl -fsSL --max-time 60 -o "$TMP/checksums.txt" "$BASE/checksums.txt" 2>/dev
|
|
|
92
110
|
echo "error: checksum mismatch for $ARCHIVE (expected $expected, got $actual)" >&2
|
|
93
111
|
exit 2
|
|
94
112
|
fi
|
|
95
|
-
echo " checksum OK"
|
|
113
|
+
echo " checksum OK ($expected)"
|
|
96
114
|
else
|
|
97
|
-
|
|
115
|
+
unverified "$ARCHIVE not listed in checksums.txt"
|
|
98
116
|
fi
|
|
99
117
|
else
|
|
100
|
-
|
|
118
|
+
unverified "checksums.txt unavailable at $BASE"
|
|
101
119
|
fi
|
|
102
120
|
|
|
103
121
|
# --- Extract and install ---
|
package/scripts/lib.sh
CHANGED
|
@@ -2,21 +2,26 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Shared validation library for Draft skill files.
|
|
4
4
|
#
|
|
5
|
-
# Sourced by test suites. Defines constants and validation
|
|
6
|
-
#
|
|
5
|
+
# Sourced by test suites and the build script. Defines constants and validation
|
|
6
|
+
# functions only — no side effects at source time, shell options included. Every
|
|
7
|
+
# consumer sets its own `set -euo pipefail`; doing it here would silently change
|
|
8
|
+
# the behaviour of whatever sourced us.
|
|
7
9
|
#
|
|
8
10
|
# Usage:
|
|
9
11
|
# source scripts/lib.sh
|
|
10
12
|
#
|
|
11
13
|
|
|
12
|
-
set -euo pipefail
|
|
13
|
-
|
|
14
14
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
15
|
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
16
16
|
SKILLS_DIR="$ROOT_DIR/skills"
|
|
17
17
|
CORE_DIR="$ROOT_DIR/core"
|
|
18
18
|
TOOLS_DIR="$ROOT_DIR/scripts/tools"
|
|
19
19
|
|
|
20
|
+
# Shared shell helpers (json_escape, apply_dest_mode, …). Side-effect free at
|
|
21
|
+
# source time, so this is safe for every consumer of lib.sh.
|
|
22
|
+
# shellcheck source=tools/_lib.sh
|
|
23
|
+
source "$TOOLS_DIR/_lib.sh"
|
|
24
|
+
|
|
20
25
|
# ─────────────────────────────────────────────────────────
|
|
21
26
|
# Skill ordering (canonical order for all references)
|
|
22
27
|
# ─────────────────────────────────────────────────────────
|
|
@@ -250,6 +255,7 @@ TOOLS=(
|
|
|
250
255
|
"validate-frontmatter.sh"
|
|
251
256
|
# Foundations hygiene/verification tools
|
|
252
257
|
"check-graph-usage-report.sh"
|
|
258
|
+
"check-repo-size.sh"
|
|
253
259
|
"check-scope-conflicts.sh"
|
|
254
260
|
"check-skill-line-caps.sh"
|
|
255
261
|
"check-template-noop.sh"
|
|
@@ -257,6 +263,7 @@ TOOLS=(
|
|
|
257
263
|
"diff-templates-vs-tracks.sh"
|
|
258
264
|
"emit-skill-metrics.sh"
|
|
259
265
|
"fix-whitespace.sh"
|
|
266
|
+
"install-smoke-test.sh"
|
|
260
267
|
"migrate-track-frontmatter.sh"
|
|
261
268
|
"render-track.sh"
|
|
262
269
|
"verify-citations.sh"
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
# `STARTS WITH`, `NOT x STARTS WITH`, `AND`, `OR`, relationship-type
|
|
13
13
|
# alternation `[:A|B]`, simple `count(x)`.
|
|
14
14
|
# UNSAFE : coalesce(), `<>` / `!=` / `<=` / `>=`, `NOT EXISTS(...)`,
|
|
15
|
-
# `NOT (pattern)`, `WITH`-grouping aggregation, multi-pattern joins
|
|
15
|
+
# `NOT (pattern)`, `WITH`-grouping aggregation, multi-pattern joins,
|
|
16
|
+
# and comparing one property against another (`a.x < b.x` — the
|
|
17
|
+
# parser wants a literal on the right and fails with "expected value
|
|
18
|
+
# at pos N"). `<` against a literal is fine.
|
|
16
19
|
# Every builder below stays inside the SAFE set.
|
|
17
20
|
#
|
|
18
21
|
# Label-agnostic on name matches: code units are :Method ⪢ :Function in OO repos;
|
|
@@ -43,7 +46,12 @@ gq_q_exists() { printf "MATCH (f {name:'%s'}) RETURN f.name AS name L
|
|
|
43
46
|
gq_q_callers() { printf "MATCH (c)-[:CALLS]->(f {name:'%s'}) RETURN c.name AS caller, c.file_path AS file LIMIT 200" "$1"; }
|
|
44
47
|
gq_q_callers_prod() { printf "MATCH (c)-[:CALLS]->(f {name:'%s'}) WHERE c.is_test=false AND NOT c.file_path STARTS WITH 'tests/' RETURN c.name AS caller, c.file_path AS file LIMIT 200" "$1"; }
|
|
45
48
|
gq_q_callers_qualified() { printf "MATCH (c)-[:CALLS]->(f {qualified_name:'%s'}) RETURN c.name AS caller, c.file_path AS file LIMIT 200" "$1"; }
|
|
46
|
-
|
|
49
|
+
# The `WHERE a.qualified_name < b.qualified_name` this used to carry was doing
|
|
50
|
+
# two jobs — drop self-loops, and emit each mutual pair once instead of twice —
|
|
51
|
+
# but the engine cannot parse a property-to-property comparison, so the query
|
|
52
|
+
# failed on every call and cycle-detect swallowed the error. Both jobs now happen
|
|
53
|
+
# in jq downstream; the limit is doubled because the raw rows come in pairs.
|
|
54
|
+
gq_q_cycles2() { printf "MATCH (a)-[:CALLS]->(b)-[:CALLS]->(a) RETURN a.qualified_name AS a, b.qualified_name AS b LIMIT 200"; }
|
|
47
55
|
gq_q_cycles3() { printf "MATCH (a)-[:CALLS]->(b)-[:CALLS]->(c)-[:CALLS]->(a) RETURN a.qualified_name AS a, b.qualified_name AS b, c.qualified_name AS c LIMIT 100"; }
|
|
48
56
|
gq_q_tests() { printf "MATCH (t)-[:TESTS]->(f {name:'%s'}) RETURN t.qualified_name AS test, t.file_path AS file LIMIT 200" "$1"; }
|
|
49
57
|
gq_q_tested_all() { printf "MATCH (t)-[:TESTS]->(f) RETURN f.qualified_name AS symbol LIMIT 2000"; }
|
|
@@ -53,7 +61,6 @@ gq_q_co_change() { printf "MATCH (a:File)-[r:FILE_CHANGES_WITH]->(b:File
|
|
|
53
61
|
gq_q_inherits() { printf "MATCH (c)-[:INHERITS]->(p) RETURN c.qualified_name AS child, p.qualified_name AS parent LIMIT 500"; }
|
|
54
62
|
gq_q_inherits_sym() { printf "MATCH (c)-[:INHERITS]->(p) WHERE c.name='%s' RETURN c.qualified_name AS child, p.qualified_name AS parent LIMIT 200" "$1"; }
|
|
55
63
|
gq_q_derived_sym() { printf "MATCH (c)-[:INHERITS]->(p) WHERE p.name='%s' RETURN c.qualified_name AS child, p.qualified_name AS parent LIMIT 200" "$1"; }
|
|
56
|
-
gq_q_writes() { printf "MATCH (f {name:'%s'})-[:WRITES]->(v) RETURN v.name AS target, v.file_path AS file LIMIT 200" "$1"; }
|
|
57
64
|
gq_q_raises() { printf "MATCH (f {name:'%s'})-[:RAISES|THROWS]->(e) RETURN e.name AS error, e.qualified_name AS qualified LIMIT 200" "$1"; }
|
|
58
65
|
gq_q_raisers() { printf "MATCH (f)-[:RAISES|THROWS]->(e {name:'%s'}) RETURN f.qualified_name AS raiser, f.file_path AS file LIMIT 200" "$1"; }
|
|
59
66
|
gq_q_node_props() { printf "MATCH (f) RETURN f.qualified_name AS q, f.complexity AS c, f.cognitive AS cog, f.is_entry_point AS ep LIMIT 10000"; }
|
package/scripts/tools/_lib.sh
CHANGED
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
|
|
6
6
|
# shellcheck shell=bash
|
|
7
7
|
|
|
8
|
+
# Give a mktemp-created replacement file the mode its destination should carry.
|
|
9
|
+
# mktemp creates 0600 and `mv` swaps the inode, so an atomic rewrite silently
|
|
10
|
+
# strips the destination's permissions (a 0644 doc came back 0600). Call this on
|
|
11
|
+
# the temp file BEFORE the mv. Uses the destination's current mode when it
|
|
12
|
+
# exists, else the mode a plain `>` redirect would have produced under the
|
|
13
|
+
# caller's umask.
|
|
14
|
+
apply_dest_mode() {
|
|
15
|
+
local tmp="$1" dest="${2:-}" mode=""
|
|
16
|
+
if [[ -n "$dest" && -e "$dest" ]]; then
|
|
17
|
+
mode="$(stat -c '%a' "$dest" 2>/dev/null || stat -f '%Lp' "$dest" 2>/dev/null || true)"
|
|
18
|
+
fi
|
|
19
|
+
[[ -n "$mode" ]] || mode="$(printf '%o' "$(( 0666 & ~0$(umask) ))")"
|
|
20
|
+
chmod "$mode" "$tmp" 2>/dev/null || true
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
json_escape() {
|
|
9
24
|
local s="$1"
|
|
10
25
|
s="${s//\\/\\\\}"
|
|
@@ -15,7 +30,6 @@ json_escape() {
|
|
|
15
30
|
printf '%s' "$s"
|
|
16
31
|
}
|
|
17
32
|
|
|
18
|
-
# Extract a top-level YAML frontmatter field value from a Markdown file.
|
|
19
33
|
# Discover track directories under a repo root (default: caller's Draft repo).
|
|
20
34
|
discover_track_dirs() {
|
|
21
35
|
local repo_root="${1:-}"
|
|
@@ -90,6 +104,7 @@ skill_line_cap() {
|
|
|
90
104
|
printf '%s' "$global_cap"
|
|
91
105
|
}
|
|
92
106
|
|
|
107
|
+
# Extract a top-level YAML frontmatter field value from a Markdown file.
|
|
93
108
|
get_yaml_field() {
|
|
94
109
|
local file="$1"
|
|
95
110
|
local key="$2"
|
|
@@ -109,11 +124,6 @@ get_yaml_field() {
|
|
|
109
124
|
' "$file"
|
|
110
125
|
}
|
|
111
126
|
|
|
112
|
-
# Locate the `codebase-memory-mcp` binary (Draft knowledge-graph engine).
|
|
113
|
-
# Sets MEMORY_BIN globally; returns 0 if found, 1 otherwise.
|
|
114
|
-
# Preference: PATH > Draft-managed install (~/.cache/draft/bin) > vendored bin/<arch> under known roots.
|
|
115
|
-
# No legacy fallbacks: the Aether `graph`/`graph-clang` binaries are retired.
|
|
116
|
-
|
|
117
127
|
# GitHub-flavored-markdown-ish heading id (shared by TOC, anchors, link fixers).
|
|
118
128
|
# Keeps underscores; strips other punctuation; collapses whitespace to '-'.
|
|
119
129
|
gfm_slug() {
|
|
@@ -124,7 +134,9 @@ gfm_slug() {
|
|
|
124
134
|
printf '%s' "$s"
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
# Count entries in an x-grounded-paths frontmatter field.
|
|
137
|
+
# Count entries in an x-grounded-paths frontmatter field. Always prints an
|
|
138
|
+
# integer — a page with no such field counts 0, not "" (callers embed the value
|
|
139
|
+
# in diagnostics, and an empty string rendered as "x-grounded-paths < 2").
|
|
128
140
|
# Supports both inline (`x-grounded-paths: ["a", "b"]`) and block lists:
|
|
129
141
|
# x-grounded-paths:
|
|
130
142
|
# - "a"
|
|
@@ -140,20 +152,26 @@ grounded_paths_count() {
|
|
|
140
152
|
sub(/^x-grounded-paths:[[:space:]]*\[/, "", line)
|
|
141
153
|
sub(/\].*$/, "", line)
|
|
142
154
|
gsub(/[[:space:]]/, "", line)
|
|
143
|
-
if (line=="") { print 0; exit }
|
|
155
|
+
if (line=="") { print 0; printed=1; exit }
|
|
144
156
|
n=split(line, a, ",")
|
|
145
157
|
print n
|
|
158
|
+
printed=1
|
|
146
159
|
exit
|
|
147
160
|
}
|
|
148
161
|
fm && /^x-grounded-paths:[[:space:]]*$/ { collect=1; next }
|
|
149
162
|
fm && collect {
|
|
150
163
|
if ($0 ~ /^[[:space:]]*-[[:space:]]*/) { n++; next }
|
|
151
|
-
if ($0 ~ /^[A-Za-z0-9_-]+:/) { print n+0; exit }
|
|
164
|
+
if ($0 ~ /^[A-Za-z0-9_-]+:/) { print n+0; printed=1; exit }
|
|
152
165
|
}
|
|
153
|
-
END { if (
|
|
166
|
+
END { if (!printed) print n+0 }
|
|
154
167
|
' "$file"
|
|
155
168
|
}
|
|
156
169
|
|
|
170
|
+
# Locate the `codebase-memory-mcp` binary (Draft knowledge-graph engine).
|
|
171
|
+
# Sets MEMORY_BIN globally; returns 0 if found, 1 otherwise.
|
|
172
|
+
# Precedence: DRAFT_MEMORY_DISABLE (hard off) > DRAFT_MEMORY_BIN > PATH >
|
|
173
|
+
# Draft-managed install (~/.cache/draft/bin) > vendored bin/<arch> under known roots.
|
|
174
|
+
# No legacy fallbacks: the Aether `graph`/`graph-clang` binaries are retired.
|
|
157
175
|
find_memory_bin() {
|
|
158
176
|
local repo_abs="$1"
|
|
159
177
|
local self_repo="$2"
|
|
@@ -298,7 +316,11 @@ _can_cgroup_bound() {
|
|
|
298
316
|
# Echoes the engine's JSON result on stdout (same contract as memory_cli).
|
|
299
317
|
memory_index_bounded() {
|
|
300
318
|
local repo_abs="$1"
|
|
301
|
-
|
|
319
|
+
# Payload built with jq (never string concatenation) so a repo path
|
|
320
|
+
# containing a `"` or `\` can never corrupt the JSON sent to the engine.
|
|
321
|
+
command -v jq >/dev/null 2>&1 || return 1
|
|
322
|
+
local json
|
|
323
|
+
json="$(jq -n --arg r "$repo_abs" '{repo_path:$r}')" || return 1
|
|
302
324
|
export CBM_WORKERS="${CBM_WORKERS:-4}"
|
|
303
325
|
local total pct
|
|
304
326
|
total="$(_total_ram_mb)"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-repo-size.sh
|
|
3
|
+
#
|
|
4
|
+
# Guards the size of the tree at HEAD.
|
|
5
|
+
#
|
|
6
|
+
# `claude plugin marketplace add <owner>/<repo>` git-clones this repository, so
|
|
7
|
+
# every byte tracked at HEAD is downloaded before the plugin can be installed.
|
|
8
|
+
# In v2.8.3 ~670 MB of audiobook .m4a files in HEAD stalled that first install
|
|
9
|
+
# step with no error message — an install-breaking regression that no test
|
|
10
|
+
# caught. This is that test.
|
|
11
|
+
#
|
|
12
|
+
# The threshold is deliberately loose (default 10 MB against a ~6 MB HEAD): it
|
|
13
|
+
# exists to catch binary assets landing in HEAD, not to police prose growth.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# scripts/tools/check-repo-size.sh # default cap, human output
|
|
17
|
+
# scripts/tools/check-repo-size.sh --max-mb 5 # custom cap
|
|
18
|
+
# scripts/tools/check-repo-size.sh --json # machine-readable
|
|
19
|
+
# scripts/tools/check-repo-size.sh --rev v3.6.0 # audit a past revision
|
|
20
|
+
#
|
|
21
|
+
# Exit codes:
|
|
22
|
+
# 0 tree at HEAD is within the cap
|
|
23
|
+
# 1 cap exceeded
|
|
24
|
+
# 2 usage / runtime error
|
|
25
|
+
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
usage() {
|
|
29
|
+
cat <<'EOF'
|
|
30
|
+
check-repo-size.sh — fail if the tree at HEAD exceeds a size cap
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--max-mb <n> Cap in megabytes (default: 10)
|
|
34
|
+
--rev <rev> Revision to measure (default: HEAD)
|
|
35
|
+
--repo <path> Repository root (default: this repo)
|
|
36
|
+
--top <n> Largest blobs to list (default: 10)
|
|
37
|
+
--json Emit JSON instead of human-readable output
|
|
38
|
+
--help, -h Show this message
|
|
39
|
+
EOF
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
|
|
43
|
+
usage
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
48
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
49
|
+
|
|
50
|
+
MAX_MB=10
|
|
51
|
+
REV="HEAD"
|
|
52
|
+
TOP=10
|
|
53
|
+
EMIT_JSON=0
|
|
54
|
+
|
|
55
|
+
while [[ $# -gt 0 ]]; do
|
|
56
|
+
case "$1" in
|
|
57
|
+
--max-mb)
|
|
58
|
+
[[ -n "${2:-}" ]] || { echo "check-repo-size: --max-mb requires a value" >&2; exit 2; }
|
|
59
|
+
MAX_MB="$2"; shift 2 ;;
|
|
60
|
+
--rev)
|
|
61
|
+
[[ -n "${2:-}" ]] || { echo "check-repo-size: --rev requires a value" >&2; exit 2; }
|
|
62
|
+
REV="$2"; shift 2 ;;
|
|
63
|
+
--repo)
|
|
64
|
+
[[ -n "${2:-}" ]] || { echo "check-repo-size: --repo requires a value" >&2; exit 2; }
|
|
65
|
+
REPO_ROOT="$2"; shift 2 ;;
|
|
66
|
+
--top)
|
|
67
|
+
[[ -n "${2:-}" ]] || { echo "check-repo-size: --top requires a value" >&2; exit 2; }
|
|
68
|
+
TOP="$2"; shift 2 ;;
|
|
69
|
+
--json) EMIT_JSON=1; shift ;;
|
|
70
|
+
--help|-h) usage; exit 0 ;;
|
|
71
|
+
*) echo "check-repo-size: unknown argument '$1'" >&2; usage >&2; exit 2 ;;
|
|
72
|
+
esac
|
|
73
|
+
done
|
|
74
|
+
|
|
75
|
+
[[ "$MAX_MB" =~ ^[0-9]+$ ]] || { echo "check-repo-size: --max-mb must be an integer, got '$MAX_MB'" >&2; exit 2; }
|
|
76
|
+
[[ "$TOP" =~ ^[0-9]+$ ]] || { echo "check-repo-size: --top must be an integer, got '$TOP'" >&2; exit 2; }
|
|
77
|
+
[[ -d "$REPO_ROOT" ]] || { echo "check-repo-size: no such directory: $REPO_ROOT" >&2; exit 2; }
|
|
78
|
+
|
|
79
|
+
cd "$REPO_ROOT"
|
|
80
|
+
git rev-parse --git-dir >/dev/null 2>&1 || { echo "check-repo-size: $REPO_ROOT is not a git repository" >&2; exit 2; }
|
|
81
|
+
git rev-parse --verify --quiet "$REV" >/dev/null || { echo "check-repo-size: unknown revision '$REV'" >&2; exit 2; }
|
|
82
|
+
|
|
83
|
+
# `ls-tree -l` gives per-blob sizes; symlinks/submodules report '-' and are skipped.
|
|
84
|
+
listing="$(git ls-tree -r -l "$REV")"
|
|
85
|
+
|
|
86
|
+
total_bytes=0
|
|
87
|
+
file_count=0
|
|
88
|
+
while read -r _mode _type _sha size _path; do
|
|
89
|
+
[[ "$size" =~ ^[0-9]+$ ]] || continue
|
|
90
|
+
total_bytes=$((total_bytes + size))
|
|
91
|
+
file_count=$((file_count + 1))
|
|
92
|
+
done <<< "$listing"
|
|
93
|
+
|
|
94
|
+
max_bytes=$((MAX_MB * 1024 * 1024))
|
|
95
|
+
status="ok"
|
|
96
|
+
[[ "$total_bytes" -gt "$max_bytes" ]] && status="over"
|
|
97
|
+
|
|
98
|
+
total_mb="$(awk -v b="$total_bytes" 'BEGIN { printf "%.2f", b / 1048576 }')"
|
|
99
|
+
|
|
100
|
+
if [[ "$EMIT_JSON" -eq 1 ]]; then
|
|
101
|
+
printf '{"rev":"%s","status":"%s","total_bytes":%d,"total_mb":%s,"max_mb":%d,"files":%d}\n' \
|
|
102
|
+
"$REV" "$status" "$total_bytes" "$total_mb" "$MAX_MB" "$file_count"
|
|
103
|
+
else
|
|
104
|
+
echo "Tree at $REV: ${total_mb} MB across $file_count files (cap: ${MAX_MB} MB)"
|
|
105
|
+
if [[ "$status" == "over" ]]; then
|
|
106
|
+
echo ""
|
|
107
|
+
echo "Largest tracked blobs:"
|
|
108
|
+
# Materialize the sorted list before slicing it. Piping straight into
|
|
109
|
+
# `head` lets head close the pipe early, which under `pipefail` surfaces
|
|
110
|
+
# sort's SIGPIPE as a hard failure — non-deterministically, depending on
|
|
111
|
+
# whether the output fit in the pipe buffer.
|
|
112
|
+
largest="$(awk '$4 ~ /^[0-9]+$/ { printf " %8.2f MB %s\n", $4 / 1048576, $5 }' <<< "$listing" | sort -rn)"
|
|
113
|
+
head -n "$TOP" <<< "$largest"
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
if [[ "$status" == "over" ]]; then
|
|
118
|
+
echo "" >&2
|
|
119
|
+
echo "FAIL: HEAD exceeds ${MAX_MB} MB. 'plugin marketplace add' clones this tree —" >&2
|
|
120
|
+
echo " oversized blobs stall install with no error. Ship large assets as" >&2
|
|
121
|
+
echo " GitHub Release attachments instead of committing them." >&2
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
exit 0
|
|
@@ -64,7 +64,6 @@ declare -a findings=()
|
|
|
64
64
|
record() { findings+=("$1|$2|$3"); over_count=$((over_count + 1)); }
|
|
65
65
|
|
|
66
66
|
while IFS= read -r path; do
|
|
67
|
-
rel="${path#"$REPO_ROOT/"}"
|
|
68
67
|
name="$(basename "$(dirname "$path")")"
|
|
69
68
|
lines="$(wc -l < "$path" | tr -d ' ')"
|
|
70
69
|
cap="$(skill_line_cap "$name" "$CAPS_CONF" "$GLOBAL_CAP")"
|
|
@@ -90,7 +89,7 @@ emit() {
|
|
|
90
89
|
printf 'OK: all skills within cap (default %d).\n' "$GLOBAL_CAP"
|
|
91
90
|
return
|
|
92
91
|
fi
|
|
93
|
-
local mode="warn-only"
|
|
92
|
+
local mode="warn-only"
|
|
94
93
|
((ENFORCE)) && mode="enforced"
|
|
95
94
|
if ((ENFORCE)); then
|
|
96
95
|
printf 'SKILL-CAPS: %d skill(s) over cap (%s):\n' \
|
|
@@ -34,8 +34,8 @@ Output (per file): {path, lang, category, is_test, is_generated, loc, module}
|
|
|
34
34
|
|
|
35
35
|
Language is inferred from extension and shebang. A file is classified as a
|
|
36
36
|
test when its path or basename matches common test patterns. It is flagged
|
|
37
|
-
generated when the first 5 lines contain "Code generated"
|
|
38
|
-
Module is the first path segment under root.
|
|
37
|
+
generated when the first 5 lines contain "Code generated", "DO NOT EDIT",
|
|
38
|
+
"@generated", or "autogenerated". Module is the first path segment under root.
|
|
39
39
|
EOF
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -58,18 +58,26 @@ graph_bootstrap "$REPO" || unavailable
|
|
|
58
58
|
# 2- and 3-node CALLS cycles. Cypher lives in _graph_queries.sh (label-agnostic;
|
|
59
59
|
# the Phase 0 fix — code units are mostly :Method, and CALLS only connects
|
|
60
60
|
# callables). LIMIT 100 caps each, so results are a sample, not exhaustive.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
#
|
|
62
|
+
# A failed query is not an empty result. Substituting `{}` and carrying on
|
|
63
|
+
# reported `source:"memory-graph"` with zero cycles — a clean bill of health
|
|
64
|
+
# derived from a query that never ran, which is exactly the true-negative
|
|
65
|
+
# confusion Guardrail 4 exists to prevent, and the one failure mode a caller
|
|
66
|
+
# cannot detect. Both queries must land for the sample to mean anything, so
|
|
67
|
+
# either failure routes to `unavailable`. gq_run only ever echoes validated
|
|
68
|
+
# JSON, so nothing downstream needs a second shape guard.
|
|
69
|
+
R2="$(gq_run "$PROJECT" "$(gq_q_cycles2)")" || unavailable
|
|
70
|
+
R3="$(gq_run "$PROJECT" "$(gq_q_cycles3)")" || unavailable
|
|
67
71
|
|
|
72
|
+
# Self-loops and duplicate orderings are filtered here rather than in Cypher:
|
|
73
|
+
# the engine rejects `a.x < b.x`, which is what the query used to rely on.
|
|
74
|
+
# A 2-cycle comes back twice (A,B and B,A), hence the doubled LIMIT upstream.
|
|
68
75
|
jq -n --argjson r2 "$R2" --argjson r3 "$R3" '
|
|
69
|
-
( ((($r2.rows) // []) | length) >=
|
|
76
|
+
( ((($r2.rows) // []) | length) >= 200
|
|
70
77
|
or ((($r3.rows) // []) | length) >= 100 ) as $trunc
|
|
78
|
+
| ( ($r2.rows // []) | map(select(.[0] != .[1])) | unique_by(sort) ) as $two
|
|
71
79
|
| {
|
|
72
|
-
cycles: (
|
|
80
|
+
cycles: ($two + ($r3.rows // [])),
|
|
73
81
|
truncated: $trunc,
|
|
74
82
|
source: "memory-graph"
|
|
75
83
|
}'
|
|
@@ -83,6 +83,9 @@ fix_file() {
|
|
|
83
83
|
return 1 # already clean — no change on disk
|
|
84
84
|
fi
|
|
85
85
|
|
|
86
|
+
# mktemp creates 0600 and `mv` swaps the inode — carry the destination's mode
|
|
87
|
+
# across, or an atomic rewrite silently strips the file's permissions.
|
|
88
|
+
apply_dest_mode "$_tmp" "$file"
|
|
86
89
|
mv -f "$_tmp" "$file"
|
|
87
90
|
return 0
|
|
88
91
|
}
|
|
@@ -127,6 +127,8 @@ if (( WRITE_PROJECT_METADATA )); then
|
|
|
127
127
|
fi
|
|
128
128
|
CMD="${GENERATED_BY:-draft:init}"
|
|
129
129
|
_tmp="$(mktemp "${META_FILE}.XXXXXX")"
|
|
130
|
+
# errexit on a failed write would otherwise leave metadata.json.XXXXXX behind.
|
|
131
|
+
trap 'rm -f "$_tmp"' EXIT
|
|
130
132
|
cat > "$_tmp" <<EOF
|
|
131
133
|
{
|
|
132
134
|
"\$schema": "Draft Project Metadata Schema",
|
|
@@ -150,7 +152,11 @@ if (( WRITE_PROJECT_METADATA )); then
|
|
|
150
152
|
"synced_to_commit": "$FULL_SHA"
|
|
151
153
|
}
|
|
152
154
|
EOF
|
|
155
|
+
# mktemp creates 0600 and `mv` swaps the inode — carry the destination's mode
|
|
156
|
+
# across, or an atomic rewrite silently strips the file's permissions.
|
|
157
|
+
apply_dest_mode "$_tmp" "$META_FILE"
|
|
153
158
|
mv -f "$_tmp" "$META_FILE"
|
|
159
|
+
trap - EXIT
|
|
154
160
|
echo "Written: $META_FILE (synced_to_commit=$FULL_SHA)"
|
|
155
161
|
exit 0
|
|
156
162
|
fi
|
|
@@ -57,7 +57,7 @@ unavailable() { echo '{"source":"unavailable"}'; exit 2; }
|
|
|
57
57
|
|
|
58
58
|
graph_bootstrap "$REPO" || unavailable
|
|
59
59
|
|
|
60
|
-
ARCH_JSON="$(memory_cli get_architecture "
|
|
60
|
+
ARCH_JSON="$(memory_cli get_architecture "$(jq -n --arg p "$PROJECT" '{project:$p, aspects:["all"]}')" || true)"
|
|
61
61
|
[[ -n "$ARCH_JSON" ]] || unavailable
|
|
62
62
|
|
|
63
63
|
# Validate it parses and looks like an architecture object before emitting.
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
# scripts/tools/graph-callers.sh --repo DIR --symbol NAME
|
|
10
10
|
# [--transitive[=N]] [--prod-only] [--qualified]
|
|
11
11
|
#
|
|
12
|
-
# Output: JSON {symbol, callers:[{name,file[,hop]}], status, source}.
|
|
12
|
+
# Output: JSON {symbol, callers:[{name,file,qualified[,hop]}], status, source}.
|
|
13
|
+
# `file` is always a path (empty when the engine does not carry one — the
|
|
14
|
+
# trace_path expander behind --transitive returns no file_path); the symbol's
|
|
15
|
+
# qualified name is in `qualified`, never smuggled into `file`.
|
|
13
16
|
# source = "memory-graph" | "unavailable"
|
|
14
17
|
# status = "ok" | "no-edges" | "no-match" | "unavailable"
|
|
15
18
|
# (fail-loud: distinguishes node-not-found from node-has-no-callers
|
|
@@ -92,7 +95,7 @@ if [[ "$TRANSITIVE" -eq 1 ]]; then
|
|
|
92
95
|
fi
|
|
93
96
|
echo "$RES" | jq --arg s "$SYMBOL" --arg st "$STATUS" '
|
|
94
97
|
{symbol:$s,
|
|
95
|
-
callers: [ (.callers // [])[] | {name:.name, file:(.qualified_name // ""), hop:(.hop // 1)} ],
|
|
98
|
+
callers: [ (.callers // [])[] | {name:.name, file:(.file_path // ""), qualified:(.qualified_name // ""), hop:(.hop // 1)} ],
|
|
96
99
|
status:$st, source:"memory-graph"}'
|
|
97
100
|
exit 0
|
|
98
101
|
fi
|
|
@@ -113,5 +116,5 @@ STATUS="$(gq_symbol_status "$PROJECT" "$SYM_ESC" "$RES")"
|
|
|
113
116
|
|
|
114
117
|
echo "$RES" | jq --arg s "$SYMBOL" --arg st "$STATUS" '
|
|
115
118
|
{symbol:$s,
|
|
116
|
-
callers: [ (.rows // [])[] | {name:.[0], file
|
|
119
|
+
callers: [ (.rows // [])[] | {name:.[0], file:(.[1] // ""), qualified:""} ],
|
|
117
120
|
status:$st, source:"memory-graph"}'
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
# Usage:
|
|
10
10
|
# scripts/tools/graph-impact.sh --repo DIR (--file PATH | --symbol NAME) [--depth N]
|
|
11
11
|
#
|
|
12
|
-
# Output: JSON {target, kind, impacted:[{name,file,hop}], source}.
|
|
12
|
+
# Output: JSON {target, kind, impacted:[{name,file,qualified,hop}], source}.
|
|
13
|
+
# `file` is always a path (empty when the engine carries none); the qualified
|
|
14
|
+
# name has its own field rather than being emitted as if it were a path.
|
|
13
15
|
# source = "memory-graph" | "unavailable"
|
|
14
16
|
#
|
|
15
17
|
# Exit codes: 0 OK, 1 invocation error, 2 graph engine unavailable.
|
|
@@ -78,7 +80,7 @@ if [[ -n "$SYMBOL" ]]; then
|
|
|
78
80
|
echo "$RES" | jq -e . >/dev/null 2>&1 || unavailable "$TARGET" "$KIND"
|
|
79
81
|
echo "$RES" | jq --arg t "$TARGET" '
|
|
80
82
|
{target:$t, kind:"symbol",
|
|
81
|
-
impacted: [ (.callers // [])[] | {name:.name, file:(.qualified_name // ""), hop:(.hop // 1)} ],
|
|
83
|
+
impacted: [ (.callers // [])[] | {name:.name, file:(.file_path // ""), qualified:(.qualified_name // ""), hop:(.hop // 1)} ],
|
|
82
84
|
source:"memory-graph"}'
|
|
83
85
|
else
|
|
84
86
|
# File impact: detect_changes maps the working-tree diff to impacted symbols.
|
|
@@ -93,7 +93,7 @@ ensure_engine() {
|
|
|
93
93
|
engine_unavailable() {
|
|
94
94
|
if [[ "$EMIT_JSON" -eq 1 ]]; then
|
|
95
95
|
printf '{"status":"unavailable","root":"%s","scope":"%s","is_root":%s}\n' \
|
|
96
|
-
"$ROOT_ABS" "$SCOPE_ABS" "$IS_ROOT"
|
|
96
|
+
"$(json_escape "$ROOT_ABS")" "$(json_escape "$SCOPE_ABS")" "$IS_ROOT"
|
|
97
97
|
else
|
|
98
98
|
echo "WARNING: knowledge-graph engine (codebase-memory-mcp) is unavailable — no graph built." >&2
|
|
99
99
|
echo " The engine is Draft's default capability tier. Install it with:" >&2
|
|
@@ -115,7 +115,7 @@ build_snapshot() {
|
|
|
115
115
|
# Path from <module>/draft/graph back to <root>/draft/graph (module is under root).
|
|
116
116
|
root_link_relpath() {
|
|
117
117
|
local sub="${SCOPE_ABS#"$ROOT_ABS"/}"
|
|
118
|
-
local ups
|
|
118
|
+
local ups seg
|
|
119
119
|
IFS='/' read -ra seg <<< "$sub"
|
|
120
120
|
ups=$(( ${#seg[@]} + 2 ))
|
|
121
121
|
local i out=""
|
|
@@ -135,13 +135,16 @@ write_root_link() {
|
|
|
135
135
|
fi
|
|
136
136
|
root_commit="$(git -C "$ROOT_ABS" rev-parse --verify --quiet HEAD 2>/dev/null || echo none)"
|
|
137
137
|
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
138
|
+
# Every interpolated value goes through json_escape: a repo path or engine
|
|
139
|
+
# project name carrying a quote or backslash would otherwise emit a
|
|
140
|
+
# root-link.json that no consumer can parse.
|
|
138
141
|
cat > "$mod_graph/root-link.json" <<EOF
|
|
139
142
|
{
|
|
140
|
-
"root_graph": "$rel",
|
|
141
|
-
"root_abs": "$ROOT_ABS/draft/graph",
|
|
142
|
-
"root_project": "${root_project:-unknown}",
|
|
143
|
-
"root_commit": "$root_commit",
|
|
144
|
-
"status": "$status",
|
|
143
|
+
"root_graph": "$(json_escape "$rel")",
|
|
144
|
+
"root_abs": "$(json_escape "$ROOT_ABS/draft/graph")",
|
|
145
|
+
"root_project": "$(json_escape "${root_project:-unknown}")",
|
|
146
|
+
"root_commit": "$(json_escape "$root_commit")",
|
|
147
|
+
"status": "$(json_escape "$status")",
|
|
145
148
|
"linked_at": "$ts",
|
|
146
149
|
"linked_by": "graph-init.sh",
|
|
147
150
|
"note": "Root is the authoritative whole-repo graph. Follow root_graph for cross-module understanding."
|
|
@@ -173,7 +176,8 @@ fi
|
|
|
173
176
|
|
|
174
177
|
if [[ "$EMIT_JSON" -eq 1 ]]; then
|
|
175
178
|
printf '{"status":"ok","root":"%s","scope":"%s","is_root":%s,"root_built":%s,"module_built":%s,"link_status":"%s"}\n' \
|
|
176
|
-
"$
|
|
179
|
+
"$(json_escape "$ROOT_ABS")" "$(json_escape "$SCOPE_ABS")" \
|
|
180
|
+
"$IS_ROOT" "$ROOT_BUILT" "$MODULE_BUILT" "$(json_escape "$LINK_STATUS")"
|
|
177
181
|
else
|
|
178
182
|
echo "--- graph-init ---"
|
|
179
183
|
echo "Root: $ROOT_ABS$([[ $IS_ROOT -eq 1 ]] && echo ' (this scope is the root)')"
|
|
@@ -168,7 +168,10 @@ if find_memory_bin "$REPO_ABS" "$SELF_REPO"; then
|
|
|
168
168
|
ENGINE_FOUND=true
|
|
169
169
|
VER="$("$ENGINE" --version 2>/dev/null | head -1 || echo '?')"
|
|
170
170
|
ok "Engine: $ENGINE ($VER)"
|
|
171
|
+
# Numeric-only: the value is emitted bare into the --json report, so a
|
|
172
|
+
# non-numeric field (engine output format drift) would produce invalid JSON.
|
|
171
173
|
LIMIT="$("$ENGINE" config list 2>/dev/null | awk '/auto_index_limit/{print $3}' || true)"
|
|
174
|
+
[[ "$LIMIT" =~ ^[0-9]+$ ]] || LIMIT=""
|
|
172
175
|
[[ -n "$LIMIT" ]] && info "auto_index_limit: $LIMIT (governs AUTO-index only; explicit index_repository should bypass)" || true
|
|
173
176
|
if [[ "$GIT_OK" -eq 1 && "${LIMIT:-}" =~ ^[0-9]+$ && "$TRACKED" -gt "$LIMIT" ]]; then
|
|
174
177
|
warn "Tracked files ($TRACKED) > auto_index_limit ($LIMIT) — confirm the explicit index isn't truncated near $LIMIT."
|