Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .claude/hooks/ask-before-risky-commands.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Claude Code PreToolUse(Bash) gate — pathscale backend service.
#
# Prompts before prod-affecting / destructive commands in ANY wrapper form
# (env VAR=val …, tool -C <dir> …, chained with ; && |, multi-line, quoted via
# bash -c '…'). For everything else it stays SILENT (exit 0, no decision), so the
# normal permission rules — permissions.allow / ask / deny in .claude/settings.json
# and the session's permission mode — decide as usual.
#
# This is one layer of defense, not a replacement for the permission system: a
# pattern match over a command string is best-effort (quoting and indirection can
# evade any blocklist). It backs up the declarative permissions.ask list in
# .claude/settings.json — KEEP THE TWO IN SYNC — and for fully autonomous runs,
# prefer OS-level sandboxing on top.
#
# Adapted from PakhomovAlexander/project-hub. Edit RISKY_WORDS for this repo;
# tooling; further branches below gate `git`/`docker push`, `git clean`, recursive
# `rm`, `find -delete`, package publishing, PR-merge/release via `gh`, and a deploy
# script run by path. Add your own command families (e.g. `ssh`, a bespoke deploy
# CLI) to RISKY_WORDS, or trim what you don't use — and mirror the change in
# permissions.ask in .claude/settings.json.
set -u

# --- the watchlist: command words that should prompt before running ----------------
RISKY_WORDS="aws|gcloud|az|kubectl|helm|terraform|terragrunt|flyctl|fly"
# -----------------------------------------------------------------------------------

# Pull the command out of the hook's stdin JSON (jq if available, else python3).
if command -v jq >/dev/null 2>&1; then
cmd="$(jq -r '.tool_input.command // ""' 2>/dev/null)"
else
cmd="$(python3 -c 'import sys, json; print(json.load(sys.stdin).get("tool_input", {}).get("command", ""))' 2>/dev/null)"
fi

# Couldn't read the command → stay neutral, let normal permission rules decide.
[ -z "$cmd" ] && exit 0

# A command word counts as "at a command position" after start-of-line, whitespace,
# ; & | ( or a quote — and ends before whitespace, a quote, ) ; & | or end-of-line —
# so `bash -c 'git push'` is still seen.
b="(^|[[:space:];&|(\"'\`])"
e="([[:space:];&|)\"'\`]|$)"
# Global options that may sit between a tool and its subcommand (git -C dir push).
opts='([[:space:]]+(-[A-Za-z-]+|[-_A-Za-z0-9]+=[^[:space:]]+|-C[[:space:]]+[^[:space:]]+))*'

re="${b}(${RISKY_WORDS})${e}"
# git push / git clean, docker push — allowing global options before the subcommand.
re="$re|${b}git${opts}[[:space:]]+(push|clean)${e}"
re="$re|${b}docker${opts}[[:space:]]+push${e}"
# recursive rm (-r / -R / -fr / --recursive), with flags/paths in any order.
re="$re|${b}rm([[:space:]]+[^[:space:]]+)*[[:space:]]+(-[A-Za-z]*[rR]|--recursive)"
# find … -delete — irreversible bulk delete.
re="$re|${b}find([[:space:]]+[^[:space:]]+)*[[:space:]]+-delete${e}"
# package publishing — ships artifacts to a registry.
re="$re|${b}(npm|pnpm|yarn|bun|cargo|gem)([[:space:]]+[^[:space:]]+)*[[:space:]]+publish${e}"
re="$re|${b}twine([[:space:]]+[^[:space:]]+)*[[:space:]]+upload${e}"
# gh mutations that merge, ship, or destroy.
re="$re|${b}gh[[:space:]]+(pr[[:space:]]+merge|repo[[:space:]]+delete|release[[:space:]]+(create|delete))${e}"
re="$re|${b}gh[[:space:]]+api([[:space:]]+[^[:space:]]+)*[[:space:]]+(-X|--method)[[:space:]]+(POST|PUT|PATCH|DELETE)${e}"
# a deploy script invoked by path, e.g. ./scripts/deploy.sh — a word-list can't see it.
re="$re|${b}([./A-Za-z0-9_-]*/)?deploy(\.[A-Za-z]+)?${e}"
# WorkTable data migrations and endpoint regeneration rewrite committed artifacts.
re="$re|${b}([./A-Za-z0-9_-]*/)?regenerate_endpoints(\.[A-Za-z]+)?${e}"

if printf '%s\n' "$cmd" | grep -Eq "$re"; then
printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"Prod-affecting / destructive command — confirm before running."}}'
fi
# No match → no output: fall through to the normal permission flow.
exit 0
44 changes: 44 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"permissions": {
"allow": [
"Bash(cargo build:*)",
"Bash(cargo check:*)",
"Bash(cargo test:*)",
"Bash(cargo fmt:*)",
"Bash(cargo clippy:*)",
"Bash(cargo tree:*)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)"
],
"ask": [
"Bash(git push:*)",
"Bash(cargo publish:*)",
"Bash(npm publish:*)",
"Bash(bun publish:*)",
"Bash(docker push:*)",
"Bash(gh pr merge:*)",
"Bash(gh release:*)",
"Bash(aws:*)",
"Bash(gcloud:*)",
"Bash(az:*)",
"Bash(kubectl:*)",
"Bash(helm:*)",
"Bash(terraform:*)",
"Bash(flyctl:*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/ask-before-risky-commands.sh\""
}
]
}
]
}
}
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ tests/data/*
!tests/data/expected/
!tests/data/persist_index_table_of_contents.wt.idx

.claude
/.claude/settings.local.json
84 changes: 84 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Working agreement — WorkTable

The operating contract for **any** coding agent working in this repository. This file is
the single source of truth for the rules: Codex, Cursor and Gemini CLI read `AGENTS.md`
natively, and Claude Code loads it through the `@AGENTS.md` import in
[`CLAUDE.md`](CLAUDE.md). **Never fork these rules into a per-vendor file.**

**Rust crate** (`worktable`).

## Invariants (don't break these)

- **Keep `cargo fmt` and `cargo clippy --all-targets` clean.** Lint failures are part of the build here, not advisory.
- **Publishing to crates.io is irreversible.** A version number can never be reused, and yanking does not delete. Run `cargo publish --dry-run` first, publish from the merged default branch, and tag the release.
- **A pre-release version (`-alpha`, `-beta`) needs an exact dependency pin.** A plain `"2.0"` requirement will not match `2.0.0-alpha.1`, so consumers must be bumped deliberately.
- **Docs describe what is true now.** If you change behaviour, update the README and any affected doc in the same change.

## Build & test

```bash
cargo build
cargo test
cargo fmt && cargo clippy --all-targets # run after every change
```

## Verification

Run what you build before reporting it done. Type-checks and tests verify code correctness,
not feature correctness — **if you can't run it, say so explicitly** rather than implying
success.

- Compare against the base branch rather than asserting: a pre-existing failing test or lint
error is not something you introduced, and saying so requires checking.
- A build that finishes suspiciously fast was cached, not rebuilt. Force a real rebuild when
the rebuild is the thing you're verifying.

## PR discipline

**Always paste the full PR URL** (`https://github.com/pathscale/WorkTable/pull/<n>`), not just the number, so it's
clickable.

<!-- DORMANT — CI-green gating. Do not follow this rule yet; re-enable it as its own project.

Why it's off: CI here does not reliably attach checks to pull requests, so
`statusCheckRollup` comes back empty and "wait for green" would teach an agent to wait on
nothing. Verify per repo before switching this on.

To enable: ensure the workflow runs on `pull_request:`, confirm checks attach to a PR, then
uncomment the rule below.

After any push or PR, **check CI and don't call it done until it's green**:

```bash
gh pr view <number> --repo pathscale/WorkTable --json statusCheckRollup
```

CI running → wait and recheck. CI failed → read the logs, fix, push, wait for green.
-->

## Keeping docs honest

Hit a factual error here — a stale path, a wrong command, a moved status? Fix it in the same
change. Don't open cosmetic rewording PRs.

Learned something durable — a gotcha, a decision, a constraint? It belongs **in this repo's
docs**, not in your agent's private memory. Repo docs are versioned, reviewable, and visible
to every agent and human; private memory dies with your machine.

## Git workflow

- **Always specify the branch when pushing**: `git push origin branch-name`
- **Branch naming**: `fix/issue-description` or `feat/issue-description`
- **No force-pushing** during PR review

## Guardrails

[`.claude/settings.json`](.claude/settings.json) and [`.claude/hooks/`](.claude/hooks/) make
Claude Code prompt a human before prod-affecting or destructive commands — pushes, publishing
to a registry, `gh pr merge`, cloud CLIs, recursive deletes, deploy scripts.

**Other agents don't get that net automatically.** Apply the same rule yourself: ask before
running any command family listed in
[`.claude/hooks/ask-before-risky-commands.sh`](.claude/hooks/ask-before-risky-commands.sh).
It is one layer of defence, not a guarantee — a pattern match over a command string is
best-effort.
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@AGENTS.md

# Claude Code notes — WorkTable

The import above is binding: [`AGENTS.md`](AGENTS.md) is the **working agreement** for this
repository, and every Claude Code session loads it automatically. Don't copy rules here —
one source of truth, no drift. Only genuinely Claude-specific wiring belongs below.

- Guardrails live in [`.claude/settings.json`](.claude/settings.json): safe read-only
commands are pre-allowed; pushes, publishing, `gh pr merge`, cloud CLIs and deploys prompt
first (`permissions.ask` plus the `PreToolUse` hook in [`.claude/hooks/`](.claude/hooks/)).
- Keep the hook's `RISKY_WORDS` and `permissions.ask` **in sync** — they back each other up.
17 changes: 17 additions & 0 deletions paper-bench/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "wt-paper-bench"
version = "0.1.0"
edition = "2024"

# Opt out of the parent WorkTable workspace
[workspace]

[dependencies]
worktable = { path = ".." }
tokio = { version = "1", features = ["full"] }
parking_lot = "0.12"
dashmap = "6"

[profile.release]
lto = "fat"
codegen-units = 1
67 changes: 67 additions & 0 deletions paper-bench/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# wt-paper-bench — benchmark harness for the CIDR 2027 paper

Fills the `\ph{...}` placeholders in the paper. Run **in your macOS terminal**
(not through Claude's sandbox — it has no crates.io access). For the paper's
*official* numbers, re-run on a quiet, dedicated Linux x86 box with cores
pinned; Mac runs are fine for development and go/no-go decisions.

## Run

```bash
cd ~/code/WorkTable/paper-bench
mkdir -p results

# 1. Lock-granularity contention matrix (~7 min: 4 modes x 5 task counts x 2 runs x 5s)
cargo run --release --bin contention | tee results/contention.csv

# 2. Specialization ablation (the make-or-break experiment)
ROWS=1000000 REPS=5 cargo run --release --bin ablation | tee results/ablation.csv

# 3. Hand-rolled baselines (Vec / RwLock<HashMap> / DashMap)
ROWS=1000000 REPS=5 cargo run --release --bin baselines | tee results/baselines.csv

# 4. Compile-time + binary-size cost of specialization
bash scripts/compile_cost.sh | tee results/compile_cost.csv
```

Knobs: `DURATION_SECS` (contention, default 5), `ROWS` (default 1M),
`REPS` (default 5, median reported).

When done, just tell Claude the results are in `paper-bench/results/` — they'll
be staged back, analyzed, and the paper tables/figures filled.

## What maps to what in the paper

| Output | Paper location |
|---|---|
| `contention.csv` | §5 lock-granularity ablation + scaling figure (C2) |
| `ablation.csv` | Table 1 (specialization ablation — C1, the thesis test) |
| `baselines.csv` | Table 2 rows: Vec/HashMap/DashMap |
| `compile_cost.csv` | §5 "cost of specialization" sentence |

Still TODO after this round: external baselines (sled, redb, LMDB/heed,
SQLite `:memory:`) as a follow-up crate, YCSB-shaped mixes, and multi-threaded
ablation runs.

## Fairness caveats to revisit before trusting Table 1

The dynamic twin (`src/dynamic.rs`) models the dynamism tax as: tagged-value
rows + runtime catalog lookup + encode/decode per access + coarse per-row
mutex. It does NOT share WorkTable's page allocator or B-tree (it uses a slot
vector + BTreeMap, which is *favorable* to the twin for point ops — so a
specialized win is conservative). A v2 twin reusing WorkTable's pages/indexes
with only the row representation dynamized would isolate the typing cost more
precisely. Review `dynamic.rs` before the official campaign.

## Notes

- The bench table (`src/lib.rs`) has an indexed column `a`, non-indexed `b`/`e`
(the disjoint contention pair), an `f64`, and a fixed-length `String` (keeps
the table on the unsized code path; same-length updates stay in place).
- `contention` modes: `disjoint` (b vs e — field locks shouldn't collide),
`overlap` (both tasks write {b,e} — must serialize), `mutex` (external
single lock), `inplace` (closure RMW). The C2 claim is
disjoint >> overlap ≈ correct, with mutex as the floor.
- First build takes a few minutes (workspace + LTO). If `cargo` complains
about the parent workspace, the `[workspace]` opt-out table in Cargo.toml
should prevent it — report the error if not.
69 changes: 69 additions & 0 deletions paper-bench/scripts/compile_cost.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Compile-time and binary-size cost of specialization (paper §5).
# Builds tiny crates with 1 and 8 worktable! tables, times cold release builds,
# reports binary sizes. Run from paper-bench/ inside the WorkTable clone.
set -euo pipefail

WT_PATH="$(cd "$(dirname "$0")/../.." && pwd)" # the WorkTable clone root
WORK=$(mktemp -d)
echo "bench,tables,cold_build_secs,binary_bytes"

gen_crate() {
local n=$1 dir=$2
mkdir -p "$dir/src"
cat > "$dir/Cargo.toml" <<EOF
[package]
name = "wt-cost-$n"
version = "0.1.0"
edition = "2024"
[workspace]
[dependencies]
worktable = { path = "$WT_PATH" }
tokio = { version = "1", features = ["full"] }
[profile.release]
lto = "fat"
codegen-units = 1
EOF
{
echo 'use worktable::prelude::*; use worktable::worktable;'
for i in $(seq 1 "$n"); do
cat <<EOF
worktable!(
name: Cost$i,
columns: {
id: u64 primary_key autoincrement,
a: u64,
b: u64,
c: f64,
d: String,
},
indexes: { a_idx_$i: a, },
queries: {
update: { UpdA$i(a) by id, },
in_place: { IncB$i(b) by id, }
}
);
EOF
done
echo 'fn main() {'
for i in $(seq 1 "$n"); do
echo " let t$i = Cost${i}WorkTable::default(); t$i.insert(Cost${i}Row{ id: t$i.get_next_pk().into(), a:1, b:1, c:1.0, d:\"x\".into() }).unwrap();"
done
echo ' println!("ok");'
echo '}'
} > "$dir/src/main.rs"
}

for n in 1 8; do
dir="$WORK/cost-$n"
gen_crate "$n" "$dir"
( cd "$dir" && cargo fetch >/dev/null 2>&1 )
start=$(date +%s.%N)
( cd "$dir" && cargo build --release >/dev/null 2>&1 )
end=$(date +%s.%N)
bin=$(find "$dir/target/release" -maxdepth 1 -type f -perm -111 -name "wt-cost-*" | head -1)
size=$(stat -f%z "$bin" 2>/dev/null || stat -c%s "$bin")
echo "compile_cost,$n,$(echo "$end $start" | awk '{printf "%.1f", $1-$2}'),$size"
done

rm -rf "$WORK"
Loading
Loading