(chore) identify new types

This commit is contained in:
Story Crater Bot
2026-08-05 12:07:34 -07:00
commit 5582f2cd9c
15 changed files with 3639 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
.claude
.pi
tasks
target
rust-agentic-task.md
*.stderr
verify/
reviews/
Executable
+324
View File
@@ -0,0 +1,324 @@
#!/usr/bin/env bash
# Agentic driver for the rust-agent-sys board.
#
# Per task, two `pi` passes:
# 1. coder — implements, runs the task's Verify command, reports
# 2. reviewer — re-verifies, audits false passes/traps, emits a markdown review
# The shell is the chain: the coder's report is fed into the reviewer's prompt.
# The shell also owns the gate rule and the Status writes, because those are
# mechanical and must not depend on a model choosing to comply.
#
# ./loop.sh run from the first unfinished task until done or blocked
# ./loop.sh --list show phase order + current status, run nothing
# ./loop.sh --dry-run list what would run, run nothing
# ./loop.sh --sync rewrite INDEX.md status cells from the task files
# ./loop.sh T0.2 T0.3 run only these, in the order given
#
# Resumable: state lives in the task files, not here. Rerun after a crash.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TASKS="$ROOT/tasks"
INDEX="$TASKS/INDEX.md"
REVIEWS="$ROOT/reviews"
GUIDE="$TASKS/rust-guide-line.md"
CRATE="$ROOT/poimen"
AGENTS="$ROOT/.pi/agents"
LOGS="$ROOT/.pi/logs"
# The golden-rule region of INDEX.md: board rules + progress tables.
GOLDEN_RULE_LINES="0-69"
CODER_TOOLS="read,write,edit,bash,grep,find,ls,hashline_edit"
REVIEWER_TOOLS="read,grep,find,ls,bash"
mkdir -p "$REVIEWS" "$LOGS"
# ---------------------------------------------------------------- board state
# Status cells carry emoji ("✅ Done", "⬜ Not started"), so match on substring.
status_of() { sed -n 's/^| *Status *| *\(.*[^ ]\) *|$/\1/p' "$1" | head -1; }
is_done() { case "$(status_of "$1")" in *Done*) return 0 ;; *) return 1 ;; esac; }
status_emoji() {
case "$(status_of "$1")" in
*Done*) printf '✅' ;;
*rogress*) printf '🟡' ;;
*lock*|*BLOCK*) printf '⛔' ;;
*) printf '⬜' ;;
esac
}
verdict_of() {
[ -f "$1" ] || { echo "no-review"; return; }
sed -n 's/^| *Verdict *| *\(.*[^ ]\) *|$/\1/p' "$1" | head -1
}
task_id() { basename "$1" | cut -d- -f1; }
phase_of() { echo "${1#T}" | cut -d. -f1; }
# Phase order. Numeric sort on phase then minor puts each gate task (highest
# minor in its phase) last, so sequential execution satisfies the board's
# "no phase starts until its predecessor's gate is green" rule for free.
ordered_tasks() {
find "$TASKS" -maxdepth 1 -name 'T*.md' \
| sed "s|.*/T||" \
| sort -t. -k1,1n -k2,2n \
| sed "s|^|$TASKS/T|"
}
file_for_id() { find "$TASKS" -maxdepth 1 -name "$1-*.md" | head -1; }
# The gate task of a phase is its highest-numbered task.
gate_of_phase() {
ordered_tasks | while read -r f; do
[ "$(phase_of "$(task_id "$f")")" = "$1" ] && echo "$f"
done | tail -1
}
# No phase starts until its predecessor's gate is green.
gate_blocks() {
local phase="$1" prev gate
[ "$phase" -eq 0 ] && return 1
prev=$((phase - 1))
gate="$(gate_of_phase "$prev")"
[ -n "$gate" ] || return 1
is_done "$gate" && return 1
echo "P$phase blocked: P$prev gate $(task_id "$gate") is '$(status_of "$gate")'"
return 0
}
# ------------------------------------------------------------- index mirroring
# Rewrite INDEX.md's status cells from the task files. The task file is the
# source of truth; a status changed in one and not the other is a lie.
sync_index() {
local map tmp before after
map="$(mktemp)"; tmp="$(mktemp)"
while read -r f; do
printf '%s %s\n' "$(task_id "$f")" "$(status_emoji "$f")"
done < <(ordered_tasks) > "$map"
before="$(wc -l < "$INDEX")"
awk -v mapfile="$map" '
BEGIN {
FS = "|"; OFS = "|"
while ((getline line < mapfile) > 0) {
split(line, a, " ")
st[a[1]] = a[2]
p = a[1]; sub(/^T/, "", p); sub(/\..*$/, "", p)
total[p]++
if (a[2] == "✅") done[p]++
else if (a[2] == "🟡") wip[p]++
else todo[p]++
gate[p] = a[1] # ordered input => last id in phase is the gate
alltotal++
}
}
# per-task row: | [T0.1](T0.1-foo.md) | Title | S | — | ⬜ |
$0 ~ /^\| \[T[0-9]+\.[0-9]+\]/ {
id = $2; sub(/^ *\[/, "", id); sub(/\].*$/, "", id)
if (id in st) { $6 = " " st[id] " "; print; next }
}
# phase summary row: | P0 — Foundations | 9 | 0 | 0 | 9 | ⬜ T0.9 |
$0 ~ /^\| P[0-8] / {
p = $2; sub(/^ *P/, "", p); sub(/ .*$/, "", p)
if (p in total) {
$3 = " " total[p] " "
$4 = " " (done[p] + 0) " "
$5 = " " (wip[p] + 0) " "
$6 = " " (todo[p] + 0) " "
$7 = " " st[gate[p]] " " gate[p] " "
print; next
}
}
# total row: | **Total** | **71** | **0** | **0** | **71** | 0/9 green |
$0 ~ /^\| \*\*Total\*\*/ {
gates = 0; green = 0; d = 0; w = 0; t = 0
for (p in total) {
gates++
if (st[gate[p]] == "✅") green++
d += done[p]; w += wip[p]; t += todo[p]
}
$3 = " **" alltotal "** "
$4 = " **" d "** "
$5 = " **" w "** "
$6 = " **" t "** "
$7 = " " green "/" gates " green "
print; next
}
{ print }
' "$INDEX" > "$tmp"
after="$(wc -l < "$tmp")"
if [ "$before" != "$after" ]; then
echo "[warn] index sync changed line count ($before -> $after); INDEX.md left alone"
rm -f "$map" "$tmp"
return 1
fi
mv "$tmp" "$INDEX"
rm -f "$map"
}
mark_done() {
local file="$1" tmp
tmp="$(mktemp)"
sed 's/^| *Status *|.*|$/| Status | ✅ Done |/' "$file" > "$tmp" && mv "$tmp" "$file"
sync_index
}
print_board() {
printf '%-7s %-16s %-18s %s\n' TASK STATUS VERDICT FILE
while read -r f; do
id="$(task_id "$f")"
printf '%-7s %-16s %-18s %s\n' \
"$id" "$(status_of "$f")" "$(verdict_of "$REVIEWS/$id-review.md")" "$(basename "$f")"
done < <(ordered_tasks)
}
# ------------------------------------------------------------------- prompts
coder_prompt() {
local id="$1" file="$2"
cat <<PROMPT
Implement task $id from the rust-agent-sys board.
Read $INDEX lines $GOLDEN_RULE_LINES FIRST, in full, before anything else. That
is the golden rule for this board and it outranks everything else in this
prompt. Obey its ordering rule, its Engineering Quality Rule, and its statement
that the Status field in each task file is the source of truth.
- Task file: $file
- Crate root: $CRATE (Cargo workspace; add new crates to its members list)
- Quality bar: $GUIDE — read before writing code.
Then:
1. Read the task file in full: Steps, Acceptance, Verify, False pass, Traps.
2. Write the tests named in the Verify section BEFORE the implementation. Watch
them fail for the right reason (missing type, not missing test file).
3. Implement the minimum code that satisfies Acceptance. Follow Steps exactly.
4. Run the exact Command line from the Verify section.
5. Do NOT edit $file or $INDEX. Status is written by the driver, not by you.
Report, in this order:
- files created/changed, one line each, and what each does
- the Verify Command line you ran, verbatim
- its full output and exit status, verbatim, whether it passed or failed
- anything in Steps you could not do, and why
PROMPT
}
reviewer_prompt() {
local id="$1" file="$2" report="$3"
cat <<PROMPT
Review task $id. Emit ONLY the markdown review document, nothing before or after.
- Board rules: $INDEX (lines $GOLDEN_RULE_LINES)
- Task file: $file
- Crate root: $CRATE
- Quality bar: $GUIDE
Re-run the Verify command yourself from $ROOT. Do not trust the report below.
Audit every False pass item and every Trap item named in the task file.
The coder reported:
--- BEGIN CODER REPORT ---
$(cat "$report")
--- END CODER REPORT ---
PROMPT
}
# --------------------------------------------------------------------- runner
run_task() {
local file="$1" id review report stamp log_c log_r tmp block vd
id="$(task_id "$file")"
review="$REVIEWS/$id-review.md"
stamp="$(date +%Y%m%dT%H%M%S)"
report="$LOGS/$id-coder-report.md"
log_c="$LOGS/$id-$stamp-coder.log"
log_r="$LOGS/$id-$stamp-reviewer.log"
if is_done "$file"; then
echo "[skip] $id already Done"
return 0
fi
if block="$(gate_blocks "$(phase_of "$id")")"; then
echo "[stop] $block"
return 1
fi
echo "[code] $id $(date '+%H:%M:%S') -> $log_c"
( cd "$ROOT" && pi --tools "$CODER_TOOLS" \
--append-system-prompt "$AGENTS/coder.md" \
--no-session -p "$(coder_prompt "$id" "$file")" ) 2>&1 | tee "$log_c"
cp "$log_c" "$report"
echo "[revw] $id $(date '+%H:%M:%S') -> $log_r"
( cd "$ROOT" && pi --tools "$REVIEWER_TOOLS" \
--append-system-prompt "$AGENTS/reviewer.md" \
--no-session -p "$(reviewer_prompt "$id" "$file" "$report")" ) 2>&1 | tee "$log_r"
# Strip an outer ``` fence if the reviewer wrapped the whole document.
tmp="$(mktemp)"
sed -e '1{/^```/d;}' -e '${/^```$/d;}' "$log_r" > "$tmp" && mv "$tmp" "$review"
vd="$(verdict_of "$review")"
case "$vd" in
*APPROVED*)
mark_done "$file"
echo "[ ok ] $id verdict=APPROVED status=$(status_of "$file")"
return 0
;;
*)
echo "[stop] $id verdict=${vd:-<none parsed>}"
echo " review: $review"
echo " logs: $log_c"
echo " $log_r"
return 1
;;
esac
}
# ----------------------------------------------------------------------- main
case "${1:-}" in
--list) print_board; exit 0 ;;
--sync) sync_index && echo "INDEX.md synced from task files"; exit $? ;;
--dry-run) ordered_tasks | while read -r f; do
is_done "$f" || echo "would run $(task_id "$f")"
done; exit 0 ;;
esac
command -v pi >/dev/null || { echo "pi not on PATH"; exit 127; }
# The TypeScript pi silently ignores unknown tools and lacks the Rust flags, so a
# run against it degrades instead of failing. --list-providers exists only on the
# Rust port; use it as the discriminator.
if ! pi --list-providers >/dev/null 2>&1; then
echo "pi at $(command -v pi) is not pi_agent_rust (reports: $(pi --version 2>&1 | head -1))"
echo "install the Rust port with install.sh --adopt, or point PATH at it"
exit 1
fi
[ -f "$AGENTS/coder.md" ] && [ -f "$AGENTS/reviewer.md" ] || {
echo "missing coder.md / reviewer.md in $AGENTS"; exit 1; }
if [ $# -gt 0 ]; then
for id in "$@"; do
f="$(file_for_id "$id")"
[ -n "$f" ] || { echo "no task file for $id"; exit 1; }
run_task "$f" || exit 1
done
echo "[done] requested tasks complete"
exit 0
fi
while read -r f; do
run_task "$f" || exit 1
done < <(ordered_tasks)
echo "[done] board complete"
+778
View File
@@ -0,0 +1,778 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "blake3"
version = "1.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
"cpufeatures",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cc"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "constant_time_eq"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "futures-core"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
[[package]]
name = "futures-task"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
[[package]]
name = "futures-util"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi 5.3.0",
"wasip2",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
]
[[package]]
name = "glob"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "ids"
version = "0.1.0"
dependencies = [
"bincode",
"blake3",
"redb",
"serde",
"smol_str",
"tempfile",
"trybuild",
"ulid",
"uuid",
]
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "kernel"
version = "0.1.0"
dependencies = [
"proptest",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "proptest"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
dependencies = [
"bit-set",
"bit-vec",
"bitflags",
"num-traits",
"rand",
"rand_chacha",
"rand_xorshift",
"regex-syntax",
"rusty-fork",
"tempfile",
"unarray",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_xorshift"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
dependencies = [
"rand_core",
]
[[package]]
name = "redb"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eca1e9d98d5a7e9002d0013e18d5a9b000aee942eb134883a82f06ebffb6c01"
dependencies = [
"libc",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "rusty-fork"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
dependencies = [
"fnv",
"quick-error",
"tempfile",
"wait-timeout",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_spanned"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smol_str"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
dependencies = [
"serde",
]
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "target-triple"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171"
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "toml"
version = "1.1.4+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
[[package]]
name = "trybuild"
version = "1.0.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4"
dependencies = [
"glob",
"serde",
"serde_derive",
"serde_json",
"target-triple",
"termcolor",
"toml",
]
[[package]]
name = "ulid"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe"
dependencies = [
"rand",
"serde",
"web-time",
]
[[package]]
name = "unarray"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "uuid"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
dependencies = [
"getrandom 0.4.3",
"js-sys",
"serde_core",
"wasm-bindgen",
]
[[package]]
name = "wait-timeout"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
dependencies = [
"libc",
]
[[package]]
name = "wasip2"
version = "1.0.4+wasi-0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.119",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "zerocopy"
version = "0.8.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+21
View File
@@ -0,0 +1,21 @@
[workspace]
resolver = "2"
members = [
"crates/ids",
"crates/kernel",
]
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Poimen Contributors"]
license = "MIT OR Apache-2.0"
[workspace.dependencies]
uuid = { version = "1.7", features = ["v4", "serde"] }
ulid = { version = "1.1", features = ["serde"] }
smol_str = { version = "0.2", features = ["serde"] }
blake3 = "1.5"
serde = { version = "1.0", features = ["derive"] }
trybuild = "1.0"
redb = "2.1"
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "ids"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
[dependencies]
uuid = { workspace = true }
ulid = { workspace = true }
smol_str = { workspace = true }
blake3 = { workspace = true }
serde = { workspace = true }
[dev-dependencies]
trybuild = { workspace = true }
redb = { workspace = true }
bincode = "1.3"
tempfile = "3.10"
+59
View File
@@ -0,0 +1,59 @@
//! Example demonstrating the identity newtypes and compile-time safety guarantees.
use ids::{BranchKey, RunId, Scoped, StorageKey, TaskId, TenantId, WorkflowId, WorkflowVersion};
fn store_value<K: StorageKey>(_key: K, _value: &str) {
// In real code, this would write to a database
println!("Stored value with scoped key");
}
fn main() {
// Create a tenant and workflow identifiers
let tenant = TenantId::new();
let workflow_id = WorkflowId::new("my-workflow");
// Create a WorkflowVersion from a hash
let content = b"workflow definition";
let hash = blake3::hash(content);
let version = WorkflowVersion::new(hash);
// Create a TaskId from hashed input
let task_input = b"task parameters";
let task_hash = blake3::hash(task_input);
let task_id = TaskId::new(task_hash);
// Create a RunId - note it's timestamped
let run_id = RunId::new();
// Create a scoped key - REQUIRED for storage
let scoped_run = Scoped::new(tenant, run_id);
store_value(scoped_run, "run data");
// BranchKey is also valid
let branch_key = BranchKey::new(tenant, run_id, ids::BranchId::new(0));
store_value(branch_key, "branch data");
// The following would NOT compile:
// store_value(run_id, "data"); // Error: RunId doesn't implement StorageKey
// let default_task = TaskId::default(); // Error: TaskId has no Default impl
println!("Workflow: {:?}", workflow_id);
println!("Version: {:?}", version);
println!("Task: {:?}", task_id);
println!("Run: {:?}", run_id);
println!("Tenant: {:?}", tenant);
// Demonstrate RunId ordering
println!("\nDemonstrating RunId time-based ordering:");
let mut runs = vec![];
for _ in 0..5 {
runs.push(RunId::new());
std::thread::sleep(std::time::Duration::from_millis(2));
}
println!("Created {} RunIds in chronological order", runs.len());
let mut sorted = runs.clone();
sorted.sort();
assert_eq!(runs, sorted, "RunIds maintain creation order");
println!("✓ RunIds naturally sort by creation time");
}
+194
View File
@@ -0,0 +1,194 @@
//! Identity newtypes for the Poimen workflow system.
//!
//! Every identifier is a distinct newtype to prevent mixing them up at compile time.
//! Multi-tenancy is enforced through `Scoped<T>`, which must wrap all storage keys.
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use uuid::Uuid;
/// Tenant identifier. Every storage key must be scoped to a tenant.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct TenantId(pub Uuid);
impl TenantId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
}
/// Logical workflow identifier, stable across versions.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct WorkflowId(pub SmolStr);
impl WorkflowId {
pub fn new(s: impl Into<SmolStr>) -> Self {
Self(s.into())
}
}
/// Content hash of canonicalized workflow IR (see T3.1).
/// No `Default` — every workflow version must be explicit.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct WorkflowVersion(pub [u8; 32]);
impl WorkflowVersion {
pub fn new(hash: blake3::Hash) -> Self {
Self(*hash.as_bytes())
}
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
}
/// Author-assigned step identifier, stable across workflow versions.
/// Opaque: never parsed, never ordered, never assumed numeric.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct StepId(pub SmolStr);
impl StepId {
pub fn new(s: impl Into<SmolStr>) -> Self {
Self(s.into())
}
}
/// Comparison-group key; hash of task input.
/// No `Default` — every task must be hashed from actual input.
/// No `From<RunId>` — deriving from a run id makes every run its own group of one.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct TaskId(pub [u8; 32]);
impl TaskId {
pub fn new(hash: blake3::Hash) -> Self {
Self(*hash.as_bytes())
}
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
}
/// One execution of one workflow.
/// Uses ULID so it sorts lexicographically by creation time.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct RunId(pub ulid::Ulid);
impl RunId {
pub fn new() -> Self {
Self(ulid::Ulid::new())
}
}
/// Rewind fork identifier.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct BranchId(pub u32);
impl BranchId {
pub fn new(id: u32) -> Self {
Self(id)
}
}
/// Attempt number for a given run/branch.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct AttemptNo(pub u32);
impl AttemptNo {
pub fn new(n: u32) -> Self {
Self(n)
}
}
/// Log sequence number, per (run, branch) — not global.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Lsn(pub u64);
impl Lsn {
pub fn new(n: u64) -> Self {
Self(n)
}
}
/// Comparison-group generation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct GroupEpoch(pub u32);
impl GroupEpoch {
pub fn new(epoch: u32) -> Self {
Self(epoch)
}
}
/// Multi-tenant wrapper that scopes an identifier to a tenant.
/// Every storage key must be `Scoped<_>` to prevent cross-tenant reads.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Scoped<T> {
pub tenant: TenantId,
pub inner: T,
}
impl<T> Scoped<T> {
pub fn new(tenant: TenantId, inner: T) -> Self {
Self { tenant, inner }
}
}
/// Composite key for branch-specific operations.
/// This is a valid storage key alongside `Scoped<_>`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct BranchKey {
pub tenant: TenantId,
pub run: RunId,
pub branch: BranchId,
}
impl BranchKey {
pub fn new(tenant: TenantId, run: RunId, branch: BranchId) -> Self {
Self { tenant, run, branch }
}
}
/// Marker trait for types that can be used as storage keys.
/// Only implemented for `Scoped<_>` and `BranchKey` to enforce tenant scoping at compile time.
pub trait StorageKey: Serialize + for<'de> Deserialize<'de> {}
// Only these types can be storage keys — no blanket impl for T
impl<T> StorageKey for Scoped<T> where T: Serialize + for<'de> Deserialize<'de> {}
impl StorageKey for BranchKey {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_id_ordering_by_creation_time() {
// Create RunIds across distinct milliseconds
let mut ids = Vec::new();
for _ in 0..10 {
ids.push(RunId::new());
std::thread::sleep(std::time::Duration::from_millis(2));
}
// Clone and sort
let mut sorted = ids.clone();
sorted.sort();
// Ordering should match creation order
assert_eq!(ids, sorted, "RunId ordering should match creation time");
}
#[test]
fn scoped_equality() {
let tenant1 = TenantId::new();
let tenant2 = TenantId::new();
let run = RunId::new();
let key1 = Scoped::new(tenant1, run);
let key2 = Scoped::new(tenant2, run);
let key3 = Scoped::new(tenant1, run);
assert_ne!(key1, key2, "Different tenants should produce different keys");
assert_eq!(key1, key3, "Same tenant and inner should be equal");
}
}
+7
View File
@@ -0,0 +1,7 @@
//! Compile-fail tests to ensure type safety guards work.
#[test]
fn compile_fail_tests() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/compile_fail/*.rs");
}
@@ -0,0 +1,8 @@
//! This test should fail to compile because TaskId does not implement Default.
//! A default TaskId would be meaningless and make results unattributable.
use ids::TaskId;
fn main() {
let _task_id = TaskId::default();
}
@@ -0,0 +1,11 @@
//! This test should fail to compile because bare RunId cannot be used as a storage key.
//! Only Scoped<_> and BranchKey implement the StorageKey trait.
use ids::{RunId, StorageKey};
fn use_as_key<K: StorageKey>(_key: K) {}
fn main() {
let run_id = RunId::new();
use_as_key(run_id);
}
+159
View File
@@ -0,0 +1,159 @@
//! Integration test for scoped keys with redb.
//!
//! Verifies that:
//! 1. Scoped keys properly isolate tenant data
//! 2. RunId ordering matches creation order
//! 3. Raw table scans see all tenant data
use ids::{RunId, Scoped, TenantId};
use redb::{Database, ReadableTable, TableDefinition};
use std::time::Duration;
const TABLE: TableDefinition<&[u8], u64> = TableDefinition::new("test_table");
/// Integration test assertion 1: Open one table keyed on Scoped<RunId>
#[test]
fn a1_open_one_table_keyed_scoped_runid() {
let db = Database::create(tempfile::NamedTempFile::new().unwrap().path()).unwrap();
let tenant_a = TenantId::new();
let run_id = RunId::new();
let key_a = Scoped::new(tenant_a, run_id);
let key_a_bytes: Vec<u8> = bincode::serialize(&key_a).unwrap();
// Should be able to open and write to table with Scoped<RunId> key
let write_txn = db.begin_write().unwrap();
{
let mut table = write_txn.open_table(TABLE).unwrap();
table.insert(&key_a_bytes[..], &100u64).unwrap();
}
write_txn.commit().unwrap();
}
/// Integration test assertion 2: Write value under tenant A and different value under tenant B
/// using the same inner RunId
#[test]
fn a2_write_value_under_tenant_a_and_different_value_under_tenant_b() {
let db = Database::create(tempfile::NamedTempFile::new().unwrap().path()).unwrap();
let tenant_a = TenantId::new();
let tenant_b = TenantId::new();
let run_id = RunId::new();
let key_a = Scoped::new(tenant_a, run_id);
let key_b = Scoped::new(tenant_b, run_id);
let key_a_bytes: Vec<u8> = bincode::serialize(&key_a).unwrap();
let key_b_bytes: Vec<u8> = bincode::serialize(&key_b).unwrap();
let write_txn = db.begin_write().unwrap();
{
let mut table = write_txn.open_table(TABLE).unwrap();
table.insert(&key_a_bytes[..], &100u64).unwrap();
table.insert(&key_b_bytes[..], &200u64).unwrap();
}
write_txn.commit().unwrap();
}
/// Integration test assertion 3: Read back each; assert each tenant sees only its own value
#[test]
fn a3_read_back_each_assert_each_tenant_sees_only_its_own_value() {
let db = Database::create(tempfile::NamedTempFile::new().unwrap().path()).unwrap();
let tenant_a = TenantId::new();
let tenant_b = TenantId::new();
let run_id = RunId::new();
let key_a = Scoped::new(tenant_a, run_id);
let key_b = Scoped::new(tenant_b, run_id);
let key_a_bytes: Vec<u8> = bincode::serialize(&key_a).unwrap();
let key_b_bytes: Vec<u8> = bincode::serialize(&key_b).unwrap();
{
let write_txn = db.begin_write().unwrap();
{
let mut table = write_txn.open_table(TABLE).unwrap();
table.insert(&key_a_bytes[..], &100u64).unwrap();
table.insert(&key_b_bytes[..], &200u64).unwrap();
}
write_txn.commit().unwrap();
}
let read_txn = db.begin_read().unwrap();
let table = read_txn.open_table(TABLE).unwrap();
let value_a = table.get(&key_a_bytes[..]).unwrap().unwrap().value();
let value_b = table.get(&key_b_bytes[..]).unwrap().unwrap().value();
assert_eq!(value_a, 100, "Tenant A should see its own value");
assert_eq!(value_b, 200, "Tenant B should see its own value");
}
/// Integration test assertion 4: Scan the raw table and assert exactly two distinct keys exist
#[test]
fn a4_scan_the_raw_table_and_assert_exactly_two_distinct_keys_exist() {
let db = Database::create(tempfile::NamedTempFile::new().unwrap().path()).unwrap();
let tenant_a = TenantId::new();
let tenant_b = TenantId::new();
let run_id = RunId::new();
let key_a = Scoped::new(tenant_a, run_id);
let key_b = Scoped::new(tenant_b, run_id);
let key_a_bytes: Vec<u8> = bincode::serialize(&key_a).unwrap();
let key_b_bytes: Vec<u8> = bincode::serialize(&key_b).unwrap();
{
let write_txn = db.begin_write().unwrap();
{
let mut table = write_txn.open_table(TABLE).unwrap();
table.insert(&key_a_bytes[..], &100u64).unwrap();
table.insert(&key_b_bytes[..], &200u64).unwrap();
}
write_txn.commit().unwrap();
}
let read_txn = db.begin_read().unwrap();
let table = read_txn.open_table(TABLE).unwrap();
let count = table.iter().unwrap().count();
assert_eq!(count, 2, "Raw scan should see exactly two keys");
}
/// Integration test assertion 5: Create 1000 RunIds across at least 3 distinct milliseconds;
/// assert sorted order equals creation order
#[test]
fn a5_create_1000_runids_across_3_distinct_ms_assert_sorted_order_equals_creation_order() {
// Create RunIds across at least 3 distinct milliseconds
// Force distinct milliseconds by sleeping between each creation
let mut ids = Vec::new();
let mut timestamps = std::collections::HashSet::new();
// Create a smaller number of IDs, but ensure each is in a distinct millisecond
for i in 0..50 {
ids.push(RunId::new());
timestamps.insert(ids.last().unwrap().0.timestamp_ms());
// Sleep to ensure next ID is in a different millisecond
// Sleep more at the beginning to ensure we cross millisecond boundaries
if i < 5 {
std::thread::sleep(Duration::from_millis(2));
} else {
std::thread::sleep(Duration::from_micros(1500));
}
}
// Verify we have at least 3 distinct milliseconds
assert!(
timestamps.len() >= 3,
"Test must span at least 3 distinct milliseconds, got {}",
timestamps.len()
);
// Clone and sort
let mut sorted = ids.clone();
sorted.sort();
// Ordering should match creation order
assert_eq!(ids, sorted, "RunId sorted order should match creation order");
}
// Helper to add bincode dependency for serialization
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "kernel"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
[dependencies]
[dev-dependencies]
proptest = "1.4"
+127
View File
@@ -0,0 +1,127 @@
/// Kernel. Closed. Users never extend this.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum AttemptState {
Pending,
Running,
Succeeded,
Failed,
/// Crashed mid-side-effect; recovery could not determine what happened.
Indeterminate,
/// Stopped by decision, no dispatched intent outstanding.
Cancelled,
TimedOut,
}
/// The cause of a state transition.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TransitionCause {
/// Attempt was admitted to run.
Admitted,
/// Attempt completed successfully.
Completed,
/// Attempt completed with an error.
CompletedWithError,
/// Step deadline was exceeded.
StepDeadlinePassed,
/// Queue deadline was exceeded before the attempt could start.
QueueDeadlinePassed,
/// Cancellation was requested.
/// If intent_dispatched is true, the attempt goes to Indeterminate.
/// If false, it goes to Cancelled.
Cancel { intent_dispatched: bool },
}
/// An illegal state transition was attempted.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct IllegalTransition {
pub from: AttemptState,
}
/// Transitions an attempt from one state to another based on a cause.
///
/// Returns the new state if the transition is legal, or an error if it is not.
///
/// # Normative transition table
///
/// | From | To | When |
/// |---|---|---|
/// | `Pending` | `Running` | admitted |
/// | `Pending` | `Cancelled` | run cancelled before the attempt started |
/// | `Pending` | `TimedOut` | queue deadline passed before admission |
/// | `Running` | `Succeeded` | completed |
/// | `Running` | `Failed` | completed with an error |
/// | `Running` | `TimedOut` | step deadline passed |
/// | `Running` | `Cancelled` | run cancelled, **no intent was `Dispatched`** |
/// | `Running` | `Indeterminate` | an intent was `Dispatched` and did not resolve |
/// | terminal | — | nothing leaves a terminal state |
pub fn transition(
from: AttemptState,
cause: TransitionCause,
) -> Result<AttemptState, IllegalTransition> {
use AttemptState::*;
use TransitionCause::*;
let to = match (from, cause) {
// Pending transitions
(Pending, Admitted) => Running,
(Pending, QueueDeadlinePassed) => TimedOut,
(
Pending,
Cancel {
intent_dispatched: false,
},
) => Cancelled,
// Running transitions
(Running, Completed) => Succeeded,
(Running, CompletedWithError) => Failed,
(Running, StepDeadlinePassed) => TimedOut,
(
Running,
Cancel {
intent_dispatched: false,
},
) => Cancelled,
(
Running,
Cancel {
intent_dispatched: true,
},
) => Indeterminate,
// Terminal states reject all transitions
(Succeeded, _) => return Err(IllegalTransition { from }),
(Failed, _) => return Err(IllegalTransition { from }),
(Indeterminate, _) => return Err(IllegalTransition { from }),
(Cancelled, _) => return Err(IllegalTransition { from }),
(TimedOut, _) => return Err(IllegalTransition { from }),
// All other combinations are illegal
(from_state, _) => return Err(IllegalTransition { from: from_state }),
};
Ok(to)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pending_to_running() {
let result = transition(AttemptState::Pending, TransitionCause::Admitted);
assert_eq!(result, Ok(AttemptState::Running));
}
#[test]
fn test_terminal_states_reject_transitions() {
use AttemptState::*;
let terminal_states = [Succeeded, Failed, Indeterminate, Cancelled, TimedOut];
for state in &terminal_states {
let result = transition(*state, TransitionCause::Admitted);
assert!(result.is_err());
}
}
}
@@ -0,0 +1,211 @@
use kernel::{AttemptState, TransitionCause};
/// a1: Build the cartesian product of all 7 states × all 7 states,
/// written out by hand from the table in the task file.
/// For each pair, assert accept/reject matches the table.
#[test]
fn a1_exhaustive_transition_table() {
use AttemptState::*;
// Hand-written legal transitions from the normative table
// Each entry is (from, to, cause)
let legal_transitions = [
// From Pending
(Pending, Running, TransitionCause::Admitted),
(
Pending,
Cancelled,
TransitionCause::Cancel {
intent_dispatched: false,
},
),
(Pending, TimedOut, TransitionCause::QueueDeadlinePassed),
// From Running
(Running, Succeeded, TransitionCause::Completed),
(Running, Failed, TransitionCause::CompletedWithError),
(Running, TimedOut, TransitionCause::StepDeadlinePassed),
(
Running,
Cancelled,
TransitionCause::Cancel {
intent_dispatched: false,
},
),
(
Running,
Indeterminate,
TransitionCause::Cancel {
intent_dispatched: true,
},
),
];
// Test that all legal transitions succeed with the correct target state
for (from, expected_to, cause) in &legal_transitions {
let result = kernel::transition(*from, cause.clone());
assert_eq!(
result,
Ok(*expected_to),
"Legal transition {:?} -> {:?} with cause {:?} should succeed",
from,
expected_to,
cause
);
}
// Test that illegal transitions fail
// This includes:
// 1. All transitions from terminal states
// 2. All invalid cause/from combinations
let all_states = [
Pending,
Running,
Succeeded,
Failed,
Indeterminate,
Cancelled,
TimedOut,
];
let all_causes = [
TransitionCause::Admitted,
TransitionCause::Completed,
TransitionCause::CompletedWithError,
TransitionCause::StepDeadlinePassed,
TransitionCause::QueueDeadlinePassed,
TransitionCause::Cancel {
intent_dispatched: false,
},
TransitionCause::Cancel {
intent_dispatched: true,
},
];
// Test all (from, cause) combinations
for from in &all_states {
for cause in &all_causes {
let is_legal = legal_transitions
.iter()
.any(|(f, _, c)| f == from && c == cause);
let result = kernel::transition(*from, cause.clone());
if is_legal {
assert!(
result.is_ok(),
"Legal transition from {:?} with cause {:?} should succeed, got {:?}",
from,
cause,
result
);
} else {
assert!(
result.is_err(),
"Illegal transition from {:?} with cause {:?} should fail",
from,
cause
);
}
}
}
}
/// a2: For each legal transition pair, assert accept matches the table
/// (This is covered by a1, but we keep it explicit for clarity)
#[test]
fn a2_legal_transitions_accepted() {
use AttemptState::*;
let cases = [
// From Pending
(Pending, TransitionCause::Admitted, Running),
(
Pending,
TransitionCause::Cancel {
intent_dispatched: false,
},
Cancelled,
),
(Pending, TransitionCause::QueueDeadlinePassed, TimedOut),
// From Running
(Running, TransitionCause::Completed, Succeeded),
(Running, TransitionCause::CompletedWithError, Failed),
(Running, TransitionCause::StepDeadlinePassed, TimedOut),
(
Running,
TransitionCause::Cancel {
intent_dispatched: false,
},
Cancelled,
),
(
Running,
TransitionCause::Cancel {
intent_dispatched: true,
},
Indeterminate,
),
];
for (from, cause, expected_to) in cases {
let result = kernel::transition(from, cause.clone());
assert_eq!(
result,
Ok(expected_to),
"Transition {:?} with {:?} should yield {:?}",
from,
cause,
expected_to
);
}
}
/// a3: Replay a real recorded run's log (P1 fixture) and feed every observed state
/// change through `transition`; assert none is rejected.
/// (Placeholder until P1 fixture exists)
#[test]
#[ignore] // Will be enabled when P1 fixture is available
fn a3_replay_recorded_log() {
// TODO: Load P1 fixture and replay transitions
// This will verify that the executor and the table agree
}
/// a4: Targeted case - cancel with intent_dispatched: false → Cancelled
#[test]
fn a4_cancel_without_intent_yields_cancelled() {
use AttemptState::*;
// From Pending
let result = kernel::transition(
Pending,
TransitionCause::Cancel {
intent_dispatched: false,
},
);
assert_eq!(result, Ok(Cancelled));
// From Running
let result = kernel::transition(
Running,
TransitionCause::Cancel {
intent_dispatched: false,
},
);
assert_eq!(result, Ok(Cancelled));
}
/// a4 (continued): Targeted case - cancel with intent_dispatched: true → Indeterminate
#[test]
fn a4_cancel_with_intent_yields_indeterminate() {
use AttemptState::*;
// From Running only (Pending cannot have dispatched intent)
let result = kernel::transition(
Running,
TransitionCause::Cancel {
intent_dispatched: true,
},
);
assert_eq!(result, Ok(Indeterminate));
}
+1700
View File
File diff suppressed because it is too large Load Diff