(chore) identify new types
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
.claude
|
||||
.pi
|
||||
tasks
|
||||
target
|
||||
rust-agentic-task.md
|
||||
|
||||
*.stderr
|
||||
|
||||
verify/
|
||||
reviews/
|
||||
@@ -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"
|
||||
Generated
+778
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user