@drafthq/draft 4.0.0 → 5.0.1
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/bin/README.md +14 -13
- package/core/shared/condensation.md +8 -7
- package/core/shared/draft-context-loading.md +1 -1
- package/core/shared/graph-query.md +25 -19
- package/core/templates/architecture.md +1 -1
- package/integrations/agents/AGENTS.md +36 -29
- package/integrations/copilot/.github/copilot-instructions.md +36 -29
- package/package.json +2 -2
- package/scripts/fetch-memory-engine.sh +43 -10
- package/scripts/tools/_graph_queries.sh +25 -11
- package/scripts/tools/_lib.sh +38 -29
- package/scripts/tools/cycle-detect.sh +9 -5
- package/scripts/tools/graph-impact.sh +63 -37
- package/scripts/tools/graph-init.sh +5 -10
- package/scripts/tools/graph-preflight.sh +6 -3
- package/scripts/tools/graph-query.sh +8 -7
- package/scripts/tools/graph-snapshot.sh +13 -28
- package/scripts/tools/graph-traces.sh +2 -2
- package/scripts/tools/hotspot-rank.sh +10 -1
- package/scripts/tools/verify-graph-binary.sh +14 -2
- package/skills/implement/SKILL.md +1 -1
|
@@ -6025,7 +6025,7 @@ After a phase passes review, refresh `metadata.json.impact` so future tracks can
|
|
|
6025
6025
|
"$DRAFT_TOOLS/graph-impact.sh" --repo . --file <path>
|
|
6026
6026
|
```
|
|
6027
6027
|
|
|
6028
|
-
Aggregate across all files: `downstream_files` =
|
|
6028
|
+
Aggregate across all files: `downstream_files` = count of the union of each query's `downstream_files` array, `downstream_modules` = union of `affected_modules`, `max_depth` = max across queries, `by_category` = sum of each query's `by_category`. If the graph is absent, leave these fields as zeros / empty arrays — the snapshot still records the directly-touched files.
|
|
6029
6029
|
|
|
6030
6030
|
3. **Write metadata.json** with the populated `impact` block and `computed_at` set to the current timestamp.
|
|
6031
6031
|
|
|
@@ -17030,7 +17030,7 @@ If `draft/graph/schema.yaml` exists, the project has automated graph analysis da
|
|
|
17030
17030
|
|
|
17031
17031
|
| File | Purpose | Content |
|
|
17032
17032
|
|------|---------|---------|
|
|
17033
|
-
| `draft/graph/schema.yaml` | Gate marker (engine
|
|
17033
|
+
| `draft/graph/schema.yaml` | Gate marker (engine metadata + point-of-index counts); presence gates graph use | YAML, ~10 lines |
|
|
17034
17034
|
|
|
17035
17035
|
Note: `.ai-context.md` embeds a condensed graph summary (`GRAPH:MODULES`, `GRAPH:HOTSPOTS`, `GRAPH:CYCLES`) for first-pass structural ground truth. Deep structural data is queried live from the engine (see Live structural queries below).
|
|
17036
17036
|
|
|
@@ -17558,7 +17558,7 @@ Any skill that mutates `architecture.md` should execute this subroutine afterwar
|
|
|
17558
17558
|
| Input | Path | Description |
|
|
17559
17559
|
|-------|------|-------------|
|
|
17560
17560
|
| architecture.md | `draft/architecture.md` | Comprehensive human-readable engineering reference (source of truth) |
|
|
17561
|
-
|
|
|
17561
|
+
| Architecture view | `scripts/tools/graph-arch.sh --repo .` | Live graph metrics for tier computation (optional — skip when `draft/graph/schema.yaml` is absent) |
|
|
17562
17562
|
|
|
17563
17563
|
## Outputs
|
|
17564
17564
|
|
|
@@ -17571,11 +17571,12 @@ Any skill that mutates `architecture.md` should execute this subroutine afterwar
|
|
|
17571
17571
|
|
|
17572
17572
|
## Target Size
|
|
17573
17573
|
|
|
17574
|
-
Compute tier from `draft
|
|
17574
|
+
Compute tier from the live architecture view (the same metrics as `draft init` Step 1.4.5 — `schema.yaml` carries only counts, not these):
|
|
17575
17575
|
|
|
17576
|
-
|
|
17577
|
-
|
|
17578
|
-
|
|
17576
|
+
ARCH=$("$DRAFT_TOOLS/graph-arch.sh" --repo .) (DRAFT_TOOLS: resolver below)
|
|
17577
|
+
M = $ARCH | jq '.packages | length' (modules)
|
|
17578
|
+
F = $ARCH | jq '[.node_labels[] | select(.label=="Function" or .label=="Method") | .count] | add // 0' (functions + methods)
|
|
17579
|
+
P = $ARCH | jq '.routes | length' (routes / RPCs)
|
|
17579
17580
|
|
|
17580
17581
|
| Tier | Label | Condition | Budget |
|
|
17581
17582
|
|------|--------|----------------------------------------|---------------|
|
|
@@ -17585,7 +17586,7 @@ Compute tier from `draft/graph/schema.yaml` after graph build:
|
|
|
17585
17586
|
| 4 | large | M≤100 AND F≤5000 AND P≤500 | 400–600 lines |
|
|
17586
17587
|
| 5 | XL | M>100 OR F>5000 OR P>500 | 600–900 lines |
|
|
17587
17588
|
|
|
17588
|
-
If `schema.yaml` does not exist: default to tier 2 (180–280 lines).
|
|
17589
|
+
If `schema.yaml` does not exist (no graph) or `graph-arch.sh` reports `source: unavailable`: default to tier 2 (180–280 lines).
|
|
17589
17590
|
|
|
17590
17591
|
- Below tier minimum: incomplete condensation — ensure all sections are represented
|
|
17591
17592
|
- Above tier maximum: insufficient compression — apply prioritization rules below
|
|
@@ -17705,7 +17706,7 @@ Before writing `draft/.ai-context.md`, verify:
|
|
|
17705
17706
|
- [ ] No references to `architecture.md` (file must be self-contained)
|
|
17706
17707
|
- [ ] All invariants from architecture.md are preserved
|
|
17707
17708
|
- [ ] Extension cookbooks are complete (an agent can follow them without other files)
|
|
17708
|
-
- [ ] Output is within tier budget bounds (compute from
|
|
17709
|
+
- [ ] Output is within tier budget bounds (compute from `graph-arch.sh` or default tier 2)
|
|
17709
17710
|
- [ ] GRAPH:HOTSPOTS present (or note "No hotspot data available" if graph absent)
|
|
17710
17711
|
- [ ] GRAPH:CYCLES present ("None ✓" or cycle list; or note if graph absent)
|
|
17711
17712
|
- [ ] GRAPH:MODULE-HOTSPOTS present for tier ≥ 3 (or note if no hotspot data)
|
|
@@ -18216,7 +18217,7 @@ DRAFT_TOOLS="${DRAFT_PLUGIN_ROOT:-$(cat ~/.cache/draft/plugin-root 2>/dev/null)}
|
|
|
18216
18217
|
| `bash "$DRAFT_TOOLS/graph-query.sh" (--cypher STR \| --tool NAME --json '{...}')` | generic read-only passthrough | `{source:"unavailable"}`, exit 2 |
|
|
18217
18218
|
| `bash "$DRAFT_TOOLS/graph-traces.sh" ingest --file F --experimental` | runtime traces (experimental write) | `{source:"unavailable"}`, exit 2 |
|
|
18218
18219
|
|
|
18219
|
-
For
|
|
18220
|
+
For an engine tool without a dedicated wrapper, use the read-only passthrough `graph-query.sh --repo . --tool <name> --json '{...}'` (project injected, write tools refused, `source:"unavailable"` on failure). Never call the engine binary directly — that skips engine resolution, the index refresh before each query, and the fail-loud contract.
|
|
18220
18221
|
|
|
18221
18222
|
### Capability wrappers & dialect limits (graph-tooling-v2)
|
|
18222
18223
|
|
|
@@ -18224,8 +18225,9 @@ All Cypher lives in `scripts/tools/_graph_queries.sh` (the single source of quer
|
|
|
18224
18225
|
truth). Wrappers are thin arg-parse → builder → fail-loud JSON. Three contracts
|
|
18225
18226
|
matter when consuming them:
|
|
18226
18227
|
|
|
18227
|
-
**Fail-loud status.** Symbol-scoped wrappers (`graph-callers`, `graph-
|
|
18228
|
-
`graph-tests --symbol`, `graph-hierarchy --symbol/--derived`,
|
|
18228
|
+
**Fail-loud status.** Symbol-scoped wrappers (`graph-callers`, `graph-impact`,
|
|
18229
|
+
`graph-snippet`, `graph-tests --symbol`, `graph-hierarchy --symbol/--derived`,
|
|
18230
|
+
`graph-errors`)
|
|
18229
18231
|
emit a `status` field that distinguishes the three real outcomes — never read a
|
|
18230
18232
|
bare `[]` as a confirmed true negative:
|
|
18231
18233
|
|
|
@@ -18238,7 +18240,7 @@ bare `[]` as a confirmed true negative:
|
|
|
18238
18240
|
|
|
18239
18241
|
**Shapeless JSON is unavailable.** `gq_run` requires `has("rows") and (.rows|type=="array")`. A bare `{}` (or any object without a `.rows` array) is not a measured empty result — wrappers emit `source:"unavailable"` and a non-zero exit. Do not read `{}` as "no callers / no cycles / no edges". `graph-impact`, `graph-callers`, and `mermaid-from-graph` also require their tool-shaped object; a failed snapshot refresh does not rewrite `schema.yaml`.
|
|
18240
18242
|
|
|
18241
|
-
**Verified engine param shapes** (engine v0.
|
|
18243
|
+
**Verified engine param shapes** (engine v0.9.0 — the runtime source of truth is
|
|
18242
18244
|
`get_graph_schema`; do not hardcode a property set):
|
|
18243
18245
|
|
|
18244
18246
|
```bash
|
|
@@ -18251,13 +18253,18 @@ get_graph_schema '{"project":P}' # → {node_labels:[{label,count,properties}
|
|
|
18251
18253
|
|
|
18252
18254
|
**Cypher dialect — keep queries inside the SAFE set:**
|
|
18253
18255
|
|
|
18254
|
-
- ✅ SAFE:
|
|
18255
|
-
`
|
|
18256
|
-
`[:
|
|
18257
|
-
|
|
18258
|
-
|
|
18259
|
-
|
|
18256
|
+
- ✅ SAFE: `=`, `<>`/`!=`, `<`, `>`, `<=`, `>=` against a literal; `STARTS WITH`,
|
|
18257
|
+
`NOT x STARTS WITH`, `AND`, `OR`; explicit and variable-length patterns
|
|
18258
|
+
(`[:R*1..3]`, fixed depth `[:R*2..2]`); relationship-type alternation `[:A|B]`;
|
|
18259
|
+
`coalesce()`; `DISTINCT`; `count(x)`, `count(DISTINCT x)`; `WITH`-grouping
|
|
18260
|
+
aggregation.
|
|
18261
|
+
- ❌ UNSAFE: comparing one property against another (`a.x < b.x`), `NOT EXISTS(...)`,
|
|
18262
|
+
`NOT (pattern)`, path variables (`p=(...)`, `length(p)`) — all rejected; and
|
|
18263
|
+
multi-pattern joins (`MATCH (a)…, (b)…`), which parse but ignore `RETURN` and
|
|
18264
|
+
`LIMIT`. `graph-query.sh --cypher` returns the engine's raw error, not a silent
|
|
18260
18265
|
empty — but the builders never emit these forms.
|
|
18266
|
+
- ⚠️ `LIMIT` applies before `DISTINCT`: `RETURN DISTINCT … LIMIT n` can return
|
|
18267
|
+
fewer than n rows while more exist. Judge truncation on raw rows.
|
|
18261
18268
|
|
|
18262
18269
|
**Caveats consumers must respect:**
|
|
18263
18270
|
|
|
@@ -18292,15 +18299,15 @@ The only committed file is the gate marker:
|
|
|
18292
18299
|
|
|
18293
18300
|
| File | Role |
|
|
18294
18301
|
|------|------|
|
|
18295
|
-
| `draft/graph/schema.yaml` | Engine
|
|
18302
|
+
| `draft/graph/schema.yaml` | Engine metadata and point-of-index counts (provenance, not authoritative). Carries **no graph data**. Its presence is the **gate** (see Pre-Check) — it signals the engine is wired for this repo. Written by `scripts/tools/graph-snapshot.sh`. |
|
|
18296
18303
|
|
|
18297
|
-
All structural data is obtained live
|
|
18304
|
+
All structural data is obtained live through the query-tool wrappers under `scripts/tools/`; `graph-query.sh --tool` covers every read-only engine tool that has no dedicated wrapper. The wrappers refresh the repo's index in the engine's own cache before each query, so no committed files are required.
|
|
18298
18305
|
|
|
18299
18306
|
### How skills query (engine is the interface; jq is optional)
|
|
18300
18307
|
|
|
18301
|
-
- **The engine is the query
|
|
18308
|
+
- **The engine is the query; the wrappers are the interface.** Every wrapper drives the engine's CLI and returns JSON. There is no other query surface.
|
|
18302
18309
|
- **Prefer the wrappers — they resolve the engine for you.** `graph-arch.sh` (architecture view: packages/routes/layers/hotspots), `graph-callers.sh`, `hotspot-rank.sh`, `graph-impact.sh`, `cycle-detect.sh`, `mermaid-from-graph.sh` return already-shaped JSON. The engine binary is usually **not on `$PATH`** (it lives under `~/.cache/draft/bin/`); the wrappers locate it via `_lib.sh:find_memory_bin`, so a skill using a wrapper needs no resolution step.
|
|
18303
|
-
- **
|
|
18310
|
+
- **Tools without a dedicated wrapper go through `graph-query.sh`:** `"$DRAFT_TOOLS/graph-query.sh" --repo . --tool search_code --json '{"pattern":"..."}'` (likewise `trace_path`, `get_graph_schema`, `index_status`). `search_graph` has its own wrapper, `graph-search.sh`.
|
|
18304
18311
|
- **`jq` is not a query tool — it only trims output.** Reach for it solely to slice a *large* response (chiefly the `get_architecture` blob) down to the field you need, for token economy. The agent can read raw JSON directly; jq is an optimization, not a requirement. Don't pipe wrapper output through jq unless you genuinely need a sub-field.
|
|
18305
18312
|
|
|
18306
18313
|
The engine uses a **unified, language-agnostic** node model — `Function`, `Method`, `Class`, `Module`, `File`, `Folder`, `Route`, `Section`, `Variable` (language is inferred from file extension) — and edges `CALLS`, `DEFINES`, `CONTAINS_FILE`, `IMPORTS`, `HTTP_CALLS`, `FILE_CHANGES_WITH`, `SEMANTICALLY_RELATED`, `SIMILAR_TO`. Each node carries `file_path` + `start_line`/`end_line` and rich `properties` (complexity, signature, parent_class), and the engine exposes full-text (`search_code`) and semantic search — none of which a committed snapshot reproduced.
|
|
@@ -18320,11 +18327,11 @@ Output: `{symbol, callers[{name, file}], source}`. Use when enumerating call sit
|
|
|
18320
18327
|
### Impact — blast radius of a file or symbol
|
|
18321
18328
|
|
|
18322
18329
|
```bash
|
|
18323
|
-
"$DRAFT_TOOLS/graph-impact.sh" --repo . --file <path> #
|
|
18324
|
-
"$DRAFT_TOOLS/graph-impact.sh" --repo . --symbol <name> # transitive callers of a function
|
|
18330
|
+
"$DRAFT_TOOLS/graph-impact.sh" --repo . --file <path> # dependents of a file: its symbols' callers + its importers
|
|
18331
|
+
"$DRAFT_TOOLS/graph-impact.sh" --repo . --symbol <name> # dependents (transitive callers) of a function
|
|
18325
18332
|
```
|
|
18326
18333
|
|
|
18327
|
-
Output: `{target, kind, impacted[{name, file, hop}], source}`. Use when sizing risk before modifying a file or symbol, especially high-fan-in hotspots.
|
|
18334
|
+
Output: `{target, kind, impacted[{name, file, qualified, hop}], downstream_files, affected_modules, max_depth, by_category{code,test}, status, truncated, source}`. `impacted` lists each dependent once at its nearest hop (default depth 3), capped at 200 with `truncated:true`; the aggregates always cover the full set. `status` is `ok`, `no-edges` (target known, nothing depends on it), or `no-match` (target unknown to the graph — check the path). Use when sizing risk before modifying a file or symbol, especially high-fan-in hotspots.
|
|
18328
18335
|
|
|
18329
18336
|
### Hotspots — fan-in ranking
|
|
18330
18337
|
|
|
@@ -18435,7 +18442,7 @@ Unlocks any edge type or node property without a purpose-built wrapper. Write ve
|
|
|
18435
18442
|
"$DRAFT_TOOLS/graph-snapshot.sh" --repo .
|
|
18436
18443
|
```
|
|
18437
18444
|
|
|
18438
|
-
Indexes the repo into the engine and writes the `draft/graph/schema.yaml` gate marker
|
|
18445
|
+
Indexes the repo into the engine and writes the `draft/graph/schema.yaml` gate marker, and prints the `detect_changes` delta (`changed_files`/`impacted_symbols`) without committing it. It writes **no** graph data. Run during `draft init` and `draft graph`, or whenever the index should be refreshed.
|
|
18439
18446
|
|
|
18440
18447
|
## Finding the Engine (Resolution + Usage Report)
|
|
18441
18448
|
|
|
@@ -18476,7 +18483,7 @@ The engine indexes C/C++, Go, Python, TypeScript/JS, and more (tree-sitter, 159
|
|
|
18476
18483
|
| No engine resolvable (or `DRAFT_MEMORY_DISABLE=1`) | Skip graph indexing in init; all skills proceed without graph data; tools emit `source: unavailable` |
|
|
18477
18484
|
| Engine present but a query fails, or returns shapeless `{}` | Treat as unavailable — never as a true-negative empty result; skills proceed without graph data |
|
|
18478
18485
|
| `draft/graph/schema.yaml` exists | Engine is wired — use live query tools as needed during the run |
|
|
18479
|
-
| Engine index out of date |
|
|
18486
|
+
| Engine index out of date | Every wrapper re-indexes incrementally (content-based, git-aware) before it queries — ~0.1 s on an unchanged repo — so live answers track the working tree, including the run's own edits. `graph-snapshot.sh` (or init) also refreshes the `schema.yaml` marker. |
|
|
18480
18487
|
|
|
18481
18488
|
</core-file>
|
|
18482
18489
|
|
|
@@ -20433,7 +20440,7 @@ graph:
|
|
|
20433
20440
|
cpp: "{high}"
|
|
20434
20441
|
go: "{approximate | high}"
|
|
20435
20442
|
stats:
|
|
20436
|
-
modules: "{N from
|
|
20443
|
+
modules: "{N from engine: get_architecture .packages}"
|
|
20437
20444
|
edges: "{total_edges from engine: get_architecture .edges}"
|
|
20438
20445
|
hotspots: "{N}"
|
|
20439
20446
|
notes: "{explicit fidelity summary from engine: get_architecture .languages/.packages}"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drafthq/draft",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "Context-Driven Development for AI coding agents — install Draft into Claude Code, Cursor, Codex, or opencode.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"draft": "cli/bin/draft.js"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
27
|
"test": "bash tests/test-cli.sh",
|
|
28
|
-
"version": "bash scripts/sync-version.sh
|
|
28
|
+
"version": "bash scripts/sync-version.sh --stage",
|
|
29
29
|
"prepublishOnly": "bash scripts/build-integrations.sh"
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
#
|
|
4
4
|
# The engine is the codebase-memory-mcp single static binary. This script fetches
|
|
5
5
|
# the release archive for the host OS/arch from GitHub Releases, verifies its
|
|
6
|
-
# SHA-256 against
|
|
7
|
-
#
|
|
6
|
+
# SHA-256 (against hashes pinned below for the default version, else against the
|
|
7
|
+
# release's checksums.txt), extracts it, and installs the binary to the
|
|
8
|
+
# Draft-managed location (~/.cache/draft/bin/codebase-memory-mcp),
|
|
8
9
|
# which scripts/tools/_lib.sh:find_memory_bin resolves.
|
|
9
10
|
#
|
|
10
11
|
# Pinned by default for reproducibility; override with CMM_VERSION (a tag, e.g.
|
|
@@ -44,9 +45,15 @@ while [[ $# -gt 0 ]]; do
|
|
|
44
45
|
done
|
|
45
46
|
|
|
46
47
|
BIN_PATH="$DEST/codebase-memory-mcp"
|
|
48
|
+
# Keep an existing install only at the requested version, so a pin bump upgrades
|
|
49
|
+
# it. "latest" cannot be compared offline; any existing binary satisfies it.
|
|
47
50
|
if [[ -x "$BIN_PATH" && $FORCE -eq 0 ]]; then
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
have="$("$BIN_PATH" --version 2>/dev/null | awk '{print $NF}' || true)"
|
|
52
|
+
if [[ "$VERSION" == "latest" || "$have" == "${VERSION#v}" ]]; then
|
|
53
|
+
echo "codebase-memory-mcp already installed at $BIN_PATH (${have:-unknown})"
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
echo "Installed engine is ${have:-unknown}; replacing it with ${VERSION}."
|
|
50
57
|
fi
|
|
51
58
|
|
|
52
59
|
# --- Detect OS / arch (mirrors the engine's own install.sh naming) ---
|
|
@@ -89,6 +96,28 @@ fi
|
|
|
89
96
|
# A mismatch is always fatal. An *absent* checksum is fatal only under
|
|
90
97
|
# DRAFT_STRICT_VERIFY=1 — otherwise it warns, so that a release without a
|
|
91
98
|
# checksums.txt does not brick the install for everyone.
|
|
99
|
+
#
|
|
100
|
+
# checksums.txt ships in the same release as the archive, so it proves only that
|
|
101
|
+
# the download is intact: a replaced release asset passes it. The pinned
|
|
102
|
+
# version's archives are checked against SHA-256 values recorded here instead
|
|
103
|
+
# (copied from its checksums.txt when the pin was bumped) — update them with
|
|
104
|
+
# DEFAULT_VERSION.
|
|
105
|
+
pinned_sha256() {
|
|
106
|
+
[[ "$VERSION" == "$DEFAULT_VERSION" ]] || return 0
|
|
107
|
+
case "$1" in
|
|
108
|
+
codebase-memory-mcp-darwin-amd64.tar.gz) echo 6af3d02a27f589901fa763d3971089337bc8c9838bbed5d0cf543ca9f1a9e543 ;;
|
|
109
|
+
codebase-memory-mcp-darwin-arm64.tar.gz) echo faa02f0404230c451a9812230394481948f80183801fa5bf67044b41c2f25ed4 ;;
|
|
110
|
+
codebase-memory-mcp-linux-amd64-portable.tar.gz) echo 8459d5c9d1457f2c82de3de307ffc7641ecbba2dde893427be1e62eca8ef9b25 ;;
|
|
111
|
+
codebase-memory-mcp-linux-arm64-portable.tar.gz) echo b0a43fdaf534073c16707d72726b73b149d4c1212034b281ee8b7b2dac755107 ;;
|
|
112
|
+
esac
|
|
113
|
+
}
|
|
114
|
+
sha256_of() {
|
|
115
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
116
|
+
sha256sum "$1" | awk '{print $1}'
|
|
117
|
+
else
|
|
118
|
+
shasum -a 256 "$1" | awk '{print $1}'
|
|
119
|
+
fi
|
|
120
|
+
}
|
|
92
121
|
STRICT="${DRAFT_STRICT_VERIFY:-0}"
|
|
93
122
|
unverified() {
|
|
94
123
|
if [[ "$STRICT" == "1" ]]; then
|
|
@@ -98,14 +127,18 @@ unverified() {
|
|
|
98
127
|
echo " warning: $1 — skipping verification (set DRAFT_STRICT_VERIFY=1 to make this fatal)" >&2
|
|
99
128
|
}
|
|
100
129
|
|
|
101
|
-
|
|
130
|
+
pinned="$(pinned_sha256 "$ARCHIVE")"
|
|
131
|
+
if [[ -n "$pinned" ]]; then
|
|
132
|
+
actual="$(sha256_of "$TMP/$ARCHIVE")"
|
|
133
|
+
if [[ "$pinned" != "$actual" ]]; then
|
|
134
|
+
echo "error: $ARCHIVE does not match the SHA-256 pinned for $VERSION (expected $pinned, got $actual)" >&2
|
|
135
|
+
exit 2
|
|
136
|
+
fi
|
|
137
|
+
echo " checksum OK (pinned $pinned)"
|
|
138
|
+
elif curl -fsSL --proto '=https' --proto-redir '=https' --max-time 60 -o "$TMP/checksums.txt" "$BASE/checksums.txt" 2>/dev/null; then
|
|
102
139
|
expected="$(grep " $ARCHIVE\$" "$TMP/checksums.txt" 2>/dev/null | awk '{print $1}' | head -1 || true)"
|
|
103
140
|
if [[ -n "$expected" ]]; then
|
|
104
|
-
|
|
105
|
-
actual="$(sha256sum "$TMP/$ARCHIVE" | awk '{print $1}')"
|
|
106
|
-
else
|
|
107
|
-
actual="$(shasum -a 256 "$TMP/$ARCHIVE" | awk '{print $1}')"
|
|
108
|
-
fi
|
|
141
|
+
actual="$(sha256_of "$TMP/$ARCHIVE")"
|
|
109
142
|
if [[ "$expected" != "$actual" ]]; then
|
|
110
143
|
echo "error: checksum mismatch for $ARCHIVE (expected $expected, got $actual)" >&2
|
|
111
144
|
exit 2
|
|
@@ -7,16 +7,20 @@
|
|
|
7
7
|
# here, not a hunt across N scripts (the Phase 0 :Function bug was duplicated
|
|
8
8
|
# across two files precisely because the Cypher was inlined).
|
|
9
9
|
#
|
|
10
|
-
# Dialect notes (engine v0.
|
|
11
|
-
# SAFE :
|
|
12
|
-
# `
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# `
|
|
16
|
-
#
|
|
17
|
-
# parser wants a literal on the right
|
|
18
|
-
#
|
|
19
|
-
#
|
|
10
|
+
# Dialect notes (engine v0.9.0, verified live against this engine):
|
|
11
|
+
# SAFE : `=`, `<>`/`!=`, `<`, `>`, `<=`, `>=` against a literal; `STARTS WITH`,
|
|
12
|
+
# `NOT x STARTS WITH`, `AND`, `OR`; explicit and variable-length
|
|
13
|
+
# patterns (`[:R*1..3]`, fixed depth `[:R*2..2]`); relationship-type
|
|
14
|
+
# alternation `[:A|B]`; `coalesce()`; `DISTINCT`; `count(x)`,
|
|
15
|
+
# `count(DISTINCT x)`; `WITH`-grouping aggregation.
|
|
16
|
+
# UNSAFE : comparing one property against another (`a.x < b.x`, `a.x = b.x` —
|
|
17
|
+
# the parser wants a literal on the right: "expected value at pos N"),
|
|
18
|
+
# `NOT EXISTS(...)`, `NOT (pattern)`, path variables (`p=(...)`,
|
|
19
|
+
# `length(p)`), and multi-pattern joins (`MATCH (a)…, (b)…` parse but
|
|
20
|
+
# ignore RETURN and LIMIT).
|
|
21
|
+
# GOTCHA : LIMIT applies before DISTINCT, so `RETURN DISTINCT … LIMIT n` can
|
|
22
|
+
# return fewer than n rows while more exist — judge truncation on raw
|
|
23
|
+
# rows. Every builder below stays inside the SAFE set.
|
|
20
24
|
#
|
|
21
25
|
# Label-agnostic on name matches: code units are :Method ⪢ :Function in OO repos;
|
|
22
26
|
# pinning :Function silently returns [] (the graph-tooling-v2 Phase 0 bug). CALLS
|
|
@@ -63,7 +67,17 @@ gq_q_inherits_sym() { printf "MATCH (c)-[:INHERITS]->(p) WHERE c.name='%s'
|
|
|
63
67
|
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"; }
|
|
64
68
|
gq_q_raises() { printf "MATCH (f {name:'%s'})-[:RAISES|THROWS]->(e) RETURN e.name AS error, e.qualified_name AS qualified LIMIT 200" "$1"; }
|
|
65
69
|
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"; }
|
|
66
|
-
|
|
70
|
+
# $1 = comma-separated list of pre-escaped, single-quoted qualified names.
|
|
71
|
+
gq_q_node_props() { printf "MATCH (f) WHERE f.qualified_name IN [%s] RETURN f.qualified_name AS q, f.complexity AS c, f.cognitive AS cog, f.is_entry_point AS ep LIMIT 1000" "$1"; }
|
|
72
|
+
# Dependents at exactly $2 CALLS hops (one query per depth: path variables are
|
|
73
|
+
# unsupported, so the hop count comes from the fixed depth). Raw rows, no
|
|
74
|
+
# DISTINCT — LIMIT applies before DISTINCT, so only a raw row count at the limit
|
|
75
|
+
# reveals truncation. The file form skips callers inside the target file itself.
|
|
76
|
+
GQ_DEP_LIMIT=5000
|
|
77
|
+
gq_q_dependents_file() { printf "MATCH (a)-[:CALLS*%s..%s]->(b) WHERE b.file_path = '%s' AND a.file_path <> '%s' RETURN a.qualified_name AS q, a.name AS name, a.file_path AS file, a.is_test AS test LIMIT %s" "$2" "$2" "$1" "$1" "$GQ_DEP_LIMIT"; }
|
|
78
|
+
gq_q_dependents_symbol() { printf "MATCH (a)-[:CALLS*%s..%s]->(b {name:'%s'}) WHERE a.name <> '%s' RETURN a.qualified_name AS q, a.name AS name, a.file_path AS file, a.is_test AS test LIMIT %s" "$2" "$2" "$1" "$1" "$GQ_DEP_LIMIT"; }
|
|
79
|
+
gq_q_importers() { printf "MATCH (a)-[:IMPORTS]->(b) WHERE b.file_path = '%s' AND a.file_path <> '%s' RETURN a.file_path AS file LIMIT %s" "$1" "$1" "$GQ_DEP_LIMIT"; }
|
|
80
|
+
gq_q_file_exists() { printf "MATCH (f) WHERE f.file_path = '%s' RETURN f.file_path AS file LIMIT 1" "$1"; }
|
|
67
81
|
gq_q_risk() { printf "MATCH (f) WHERE f.unguarded_recursion=true OR f.alloc_in_loop=true OR f.recursion_in_loop=true OR f.linear_scan_in_loop=true RETURN f.qualified_name AS symbol, f.file_path AS file, f.complexity AS complexity, f.unguarded_recursion AS unguarded_recursion, f.alloc_in_loop AS alloc_in_loop, f.recursion_in_loop AS recursion_in_loop, f.linear_scan_in_loop AS linear_scan_in_loop LIMIT 200"; }
|
|
68
82
|
|
|
69
83
|
# ── Runner + classifier ──
|
package/scripts/tools/_lib.sh
CHANGED
|
@@ -235,7 +235,7 @@ find_memory_bin() {
|
|
|
235
235
|
# its own unavailable-JSON shape (the shapes differ per tool).
|
|
236
236
|
graph_bootstrap() {
|
|
237
237
|
local repo="$1" self_repo
|
|
238
|
-
REPO_ABS="$(cd "$repo" 2>/dev/null && pwd)" || return 1
|
|
238
|
+
REPO_ABS="$(cd "$repo" 2>/dev/null && pwd -P)" || return 1
|
|
239
239
|
self_repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
240
240
|
find_memory_bin "$REPO_ABS" "$self_repo" || return 1
|
|
241
241
|
command -v jq >/dev/null 2>&1 || return 1
|
|
@@ -245,6 +245,8 @@ graph_bootstrap() {
|
|
|
245
245
|
|
|
246
246
|
# Run a codebase-memory-mcp CLI tool. Echoes the JSON result (stdout); the engine's
|
|
247
247
|
# `level=...` log lines go to stderr and are discarded unless DRAFT_MEMORY_DEBUG is set.
|
|
248
|
+
# Args travel on stdin: the engine deprecated positional raw JSON (0.9.0 warns it
|
|
249
|
+
# "will be removed in a future release"), and that warning lands in the discarded stderr.
|
|
248
250
|
# Usage: memory_cli <tool> [json-args]
|
|
249
251
|
memory_cli() {
|
|
250
252
|
local tool="$1"
|
|
@@ -253,22 +255,12 @@ memory_cli() {
|
|
|
253
255
|
return 1
|
|
254
256
|
fi
|
|
255
257
|
if [[ -n "${DRAFT_MEMORY_DEBUG:-}" ]]; then
|
|
256
|
-
"$MEMORY_BIN" cli "$tool" "$args"
|
|
258
|
+
"$MEMORY_BIN" cli "$tool" <<< "$args"
|
|
257
259
|
else
|
|
258
|
-
"$MEMORY_BIN" cli "$tool" "$args" 2>/dev/null
|
|
260
|
+
"$MEMORY_BIN" cli "$tool" <<< "$args" 2>/dev/null
|
|
259
261
|
fi
|
|
260
262
|
}
|
|
261
263
|
|
|
262
|
-
# Resolve the engine's project name for a repository absolute path via list_projects.
|
|
263
|
-
# Echoes the project name, or nothing if the repo has not been indexed yet.
|
|
264
|
-
memory_project_for_repo() {
|
|
265
|
-
local repo_abs="$1"
|
|
266
|
-
command -v jq >/dev/null 2>&1 || return 1
|
|
267
|
-
memory_cli list_projects '{}' 2>/dev/null \
|
|
268
|
-
| jq -r --arg p "$repo_abs" '.projects[]? | select(.root_path == $p) | .name' 2>/dev/null \
|
|
269
|
-
| head -1
|
|
270
|
-
}
|
|
271
|
-
|
|
272
264
|
# Total physical RAM in MB (portable). Echoes a positive integer, or nothing.
|
|
273
265
|
_total_ram_mb() {
|
|
274
266
|
if [[ -r /proc/meminfo ]]; then
|
|
@@ -306,52 +298,69 @@ _can_cgroup_bound() {
|
|
|
306
298
|
|
|
307
299
|
# Index a repository under a memory bound. The codebase-memory-mcp engine
|
|
308
300
|
# self-budgets ~50% of *physical* RAM and is not cgroup-aware, so a first index
|
|
309
|
-
# of a huge repo can exhaust the host (the original 30 GB hang).
|
|
310
|
-
#
|
|
311
|
-
#
|
|
312
|
-
#
|
|
313
|
-
#
|
|
301
|
+
# of a huge repo can exhaust the host (the original 30 GB hang). The engine's
|
|
302
|
+
# own budget (CBM_MEM_BUDGET_MB) is set to DRAFT_INDEX_MEM_PCT (default 25) of
|
|
303
|
+
# total RAM unless the user chose one; on Linux the process is also confined to
|
|
304
|
+
# a transient cgroup scope of that size. CBM_WORKERS caps the engine's parallel
|
|
305
|
+
# working set so the throttle has less transient pressure to absorb. Where cgroup
|
|
306
|
+
# v2 + systemd-run are unavailable (e.g. macOS) the budget and worker cap are the
|
|
307
|
+
# bound. Never falls back
|
|
314
308
|
# from a started scope to an unbounded run — a bounded OOM fails the index
|
|
315
309
|
# cleanly (host stays alive) rather than re-triggering the hang.
|
|
316
310
|
# Echoes the engine's JSON result on stdout (same contract as memory_cli).
|
|
311
|
+
# Usage: memory_index_bounded <repo-abs> [project-name]
|
|
317
312
|
memory_index_bounded() {
|
|
318
|
-
local repo_abs="$1"
|
|
313
|
+
local repo_abs="$1" name="${2:-}"
|
|
319
314
|
# Payload built with jq (never string concatenation) so a repo path
|
|
320
315
|
# containing a `"` or `\` can never corrupt the JSON sent to the engine.
|
|
321
316
|
command -v jq >/dev/null 2>&1 || return 1
|
|
322
317
|
local json
|
|
323
|
-
json="$(jq -n --arg r "$repo_abs"
|
|
318
|
+
json="$(jq -n --arg r "$repo_abs" --arg n "$name" \
|
|
319
|
+
'{repo_path:$r} + (if $n == "" then {} else {name:$n} end)')" || return 1
|
|
324
320
|
export CBM_WORKERS="${CBM_WORKERS:-4}"
|
|
325
321
|
local total pct
|
|
326
322
|
total="$(_total_ram_mb)"
|
|
327
323
|
pct="${DRAFT_INDEX_MEM_PCT:-25}"
|
|
324
|
+
[[ "${total:-0}" -gt 0 ]] && export CBM_MEM_BUDGET_MB="${CBM_MEM_BUDGET_MB:-$(( total * pct / 100 ))}"
|
|
328
325
|
if [[ "${total:-0}" -gt 0 ]] && _can_cgroup_bound; then
|
|
329
326
|
local high_arg max_arg
|
|
330
327
|
read -r high_arg max_arg <<< "$(_mem_bound_args "$total" "$pct")"
|
|
331
328
|
if [[ -n "${DRAFT_MEMORY_DEBUG:-}" ]]; then
|
|
332
329
|
systemd-run --user --scope -q -p "$high_arg" -p "$max_arg" \
|
|
333
|
-
-- "$MEMORY_BIN" cli index_repository "$json"
|
|
330
|
+
-- "$MEMORY_BIN" cli index_repository <<< "$json"
|
|
334
331
|
else
|
|
335
332
|
systemd-run --user --scope -q -p "$high_arg" -p "$max_arg" \
|
|
336
|
-
-- "$MEMORY_BIN" cli index_repository "$json" 2>/dev/null
|
|
333
|
+
-- "$MEMORY_BIN" cli index_repository <<< "$json" 2>/dev/null
|
|
337
334
|
fi
|
|
338
335
|
else
|
|
339
336
|
memory_cli index_repository "$json"
|
|
340
337
|
fi
|
|
341
338
|
}
|
|
342
339
|
|
|
343
|
-
#
|
|
344
|
-
#
|
|
340
|
+
# Bring a repository's engine index up to date; echo its project name.
|
|
341
|
+
# Always re-indexes: the engine indexes incrementally (content-based, git-aware),
|
|
342
|
+
# so an unchanged repo costs ~0.1 s. Indexing only when the project was absent
|
|
343
|
+
# left every live query answering from the first index ever taken — a symbol
|
|
344
|
+
# added since stayed invisible while the result still said status:"ok".
|
|
345
|
+
#
|
|
346
|
+
# The project is named explicitly: the engine derives names by flattening '/' to
|
|
347
|
+
# '-', so /x/a-b/c and /x/a/b-c shared one DB and each index overwrote the other.
|
|
348
|
+
# A repo the engine already knows keeps its name (no forced full re-index); a new
|
|
349
|
+
# one gets <basename>-<sha8 of its path>, which no other path can derive.
|
|
350
|
+
# Returns 1 if the engine is unavailable.
|
|
345
351
|
memory_ensure_index() {
|
|
346
352
|
local repo_abs="$1"
|
|
347
353
|
[[ -n "${MEMORY_BIN:-}" ]] || return 1
|
|
348
354
|
command -v jq >/dev/null 2>&1 || return 1
|
|
349
|
-
local proj
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
355
|
+
local proj name
|
|
356
|
+
name="$(memory_cli list_projects '{}' \
|
|
357
|
+
| jq -r --arg p "$repo_abs" 'first(.projects[]? | select(.root_path == $p) | .name) // empty' 2>/dev/null || true)"
|
|
358
|
+
if [[ -z "$name" ]]; then
|
|
359
|
+
name="$(printf '%s' "$repo_abs" | { sha256sum 2>/dev/null || shasum -a 256; } | cut -c1-8)"
|
|
360
|
+
name="$(basename "$repo_abs")-$name"
|
|
354
361
|
fi
|
|
362
|
+
proj="$(memory_index_bounded "$repo_abs" "$name" \
|
|
363
|
+
| jq -r '.project // empty' 2>/dev/null || true)"
|
|
355
364
|
[[ -n "$proj" ]] || return 1
|
|
356
365
|
printf '%s' "$proj"
|
|
357
366
|
}
|
|
@@ -69,15 +69,19 @@ graph_bootstrap "$REPO" || unavailable
|
|
|
69
69
|
R2="$(gq_run "$PROJECT" "$(gq_q_cycles2)")" || unavailable
|
|
70
70
|
R3="$(gq_run "$PROJECT" "$(gq_q_cycles3)")" || unavailable
|
|
71
71
|
|
|
72
|
-
#
|
|
73
|
-
# the engine rejects `a.x < b.x`, which is what the query used to rely on.
|
|
74
|
-
# A
|
|
72
|
+
# Degenerate rows and duplicate rotations are filtered here rather than in
|
|
73
|
+
# Cypher: the engine rejects `a.x < b.x`, which is what the query used to rely on.
|
|
74
|
+
# A cycle comes back once per rotation (a 2-cycle as A,B and B,A — hence the
|
|
75
|
+
# doubled LIMIT upstream), and a self-loop also matches both patterns with a
|
|
76
|
+
# repeated node, e.g. (x, x, x). Drop rows that repeat a node, rotate each to
|
|
77
|
+
# start at its smallest member, and keep one of each.
|
|
75
78
|
jq -n --argjson r2 "$R2" --argjson r3 "$R3" '
|
|
79
|
+
def cycles: map(select((unique | length) == length)
|
|
80
|
+
| (indices(min)[0]) as $i | .[$i:] + .[:$i]) | unique;
|
|
76
81
|
( ((($r2.rows) // []) | length) >= 200
|
|
77
82
|
or ((($r3.rows) // []) | length) >= 100 ) as $trunc
|
|
78
|
-
| ( ($r2.rows // []) | map(select(.[0] != .[1])) | unique_by(sort) ) as $two
|
|
79
83
|
| {
|
|
80
|
-
cycles: ($
|
|
84
|
+
cycles: ((($r2.rows // []) | cycles) + (($r3.rows // []) | cycles)),
|
|
81
85
|
truncated: $trunc,
|
|
82
86
|
source: "memory-graph"
|
|
83
87
|
}'
|