diff --git a/.github/workflows/gate-topology.yml b/.github/workflows/gate-topology.yml index 5a47dc19b3..c384b839fe 100644 --- a/.github/workflows/gate-topology.yml +++ b/.github/workflows/gate-topology.yml @@ -165,3 +165,42 @@ jobs: done; } | /tmp/feed > /tmp/review.json 2> /tmp/stats || { echo "::warning::claw/suggest could not build a review"; exit 0; } cat /tmp/stats >> "$GITHUB_STEP_SUMMARY" if [ -s /tmp/review.json ]; then gh api -X POST "repos/$R/pulls/$N/reviews" --input /tmp/review.json -q .html_url >> "$GITHUB_STEP_SUMMARY"; fi + + # #10397: after a push to master, bring every conflict-free open PR that fell behind up to date. + # The decision (which PRs, which order, the cap of 30, every skip) is specs/claw/refresh.t27 lowered + # to gen/c/claw/refresh.c; this job lists, feeds the C, and calls the endpoint for each `R` line. + # The token is the update-branch row of specs/claw/github_surface.t27: pull_requests write and + # nothing else. If GitHub answers 403 here, the App also needs contents:write for update-branch + # (REST docs); the failure is a warning with GitHub's own text, never a red job. + claw-refresh: + if: github.event_name == 'push' + runs-on: ubuntu-24.04 + permissions: {contents: read, pull-requests: read} + concurrency: {group: claw-refresh, cancel-in-progress: true} + steps: + - id: have + env: {K: "${{ secrets.CLAW_APP_PRIVATE_KEY }}", I: "${{ vars.CLAW_APP_ID || secrets.CLAW_APP_ID }}"} + run: if [ -n "$K" ] && [ -n "$I" ]; then echo ok=1 >> "$GITHUB_OUTPUT"; else echo "claw/refresh updated nothing - secret CLAW_APP_PRIVATE_KEY or CLAW_APP_ID (variable or secret) is not set (docs/claw/README.md)." >> "$GITHUB_STEP_SUMMARY"; fi + - {id: tok, if: steps.have.outputs.ok, uses: actions/create-github-app-token@v2, with: {app-id: "${{ vars.CLAW_APP_ID || secrets.CLAW_APP_ID }}", private-key: "${{ secrets.CLAW_APP_PRIVATE_KEY }}", owner: gHashTag, repositories: t27, permission-pull-requests: write}} + - {if: steps.have.outputs.ok, uses: actions/checkout@v6, with: {persist-credentials: false}} + - if: steps.have.outputs.ok + env: {GH_TOKEN: "${{ github.token }}", CLAW_TOKEN: "${{ steps.tok.outputs.token }}", R: "${{ github.repository }}"} + run: | + printf '#include "gen/c/claw/refresh.c"\n#include \n#include \nint main(void){size_t k=1<<22,n;uint8_t*b=malloc(k),*o=malloc(k);n=fread(b,1,k,stdin);if(n==k)return 3;size_t m=refresh_plan(b,n,o,k);if(m>k)return 4;fwrite(o,1,m,stdout);return 0;}\n' | cc -O1 -w -I . -x c -o /tmp/feed - + list() { gh api graphql --paginate -F owner="${R%/*}" -F name="${R#*/}" -f query='query($owner:String!,$name:String!,$endCursor:String){repository(owner:$owner,name:$name){pullRequests(states:OPEN,first:100,after:$endCursor,orderBy:{field:CREATED_AT,direction:ASC}){pageInfo{hasNextPage endCursor} nodes{number headRefOid createdAt isDraft isCrossRepository maintainerCanModify mergeable mergeStateStatus baseRefName headRepository{nameWithOwner} labels(first:50){nodes{name}}}}}}' --jq '.data.repository.pullRequests.nodes[]|[.number,.headRefOid,.createdAt,.isDraft,([.labels.nodes[].name]|join(",")),.isCrossRepository,(.headRepository.nameWithOwner//""),.maintainerCanModify,.mergeable,.mergeStateStatus,.baseRefName]|@tsv'; } + # GitHub computes mergeability lazily after a push: ask once, wait, ask again. + list > /dev/null + sleep 30 + list > /tmp/prs.tsv + # Copy the facts; behind_by is the one thing the listing lacks (the ruleset is not strict, so no PR is ever reported BEHIND). + while IFS= read -r line; do + sha=$(printf '%s' "$line" | cut -f2); base=$(printf '%s' "$line" | cut -f11) + bh=$(gh api "repos/$R/compare/$base...$sha" -q .behind_by 2>/dev/null /tmp/plan || { echo "::warning::claw/refresh could not build a plan"; exit 0; } + while IFS=$'\t' read -r k a b; do + case "$k" in + R) if err=$(GH_TOKEN="$CLAW_TOKEN" gh api -X PUT "repos/$R/pulls/$a/update-branch" -f expected_head_sha="$b" 2>&1 > "$GITHUB_STEP_SUMMARY";; + esac + done < /tmp/plan diff --git a/.github/workflows/spec-guards.yml b/.github/workflows/spec-guards.yml index f22aa89fc8..9c07bd6f79 100644 --- a/.github/workflows/spec-guards.yml +++ b/.github/workflows/spec-guards.yml @@ -128,9 +128,10 @@ jobs: ./target/release/t27c gen-c specs/claw/t27f_gate.t27 > gen/c/claw/t27f_gate.c ./target/release/t27c gen-c specs/claw/sarif_feed.t27 > gen/c/claw/sarif_feed.c ./target/release/t27c gen-c specs/claw/suggest_feed.t27 > gen/c/claw/suggest_feed.c + ./target/release/t27c gen-c specs/claw/refresh.t27 > gen/c/claw/refresh.c ./target/release/t27c gen-rust specs/tri/fmt_source.t27 > bootstrap/gen/rust/tri/fmt_source.rs ./target/release/t27c gen-json specs/igla/own_model_board.t27 > gen/json/igla/own_model_board.json - git diff --exit-code -- gen/c/claw/gates.c gen/c/claw/t27f_gate.c gen/c/claw/sarif_feed.c gen/c/claw/suggest_feed.c bootstrap/gen/rust/tri/fmt_source.rs gen/json/igla/own_model_board.json + git diff --exit-code -- gen/c/claw/gates.c gen/c/claw/t27f_gate.c gen/c/claw/sarif_feed.c gen/c/claw/suggest_feed.c gen/c/claw/refresh.c bootstrap/gen/rust/tri/fmt_source.rs gen/json/igla/own_model_board.json # #7565: test only the specs this change can affect. specs/ci/affected.t27 picks them (the # changed specs and every spec that imports one through `use`, transitively); the graph is the diff --git a/.trinity/seals/claw_ClawRefresh.json b/.trinity/seals/claw_ClawRefresh.json new file mode 100644 index 0000000000..e6879c8780 --- /dev/null +++ b/.trinity/seals/claw_ClawRefresh.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+cd19e1370", + "closure": [ + { + "spec": "specs/claw/sarif.t27", + "spec_hash": "sha256:22258cfd518c5b27ad46db8bf5c507d97e783bd79073f83ff26b20549c927db8" + }, + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:b65da69dff231c61a6d3a48558af9e17af42e1acbe3f4763348f1f0c2f118f21" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:1bc4f958fdddeb6935166a07e1059847cf77d92180371bfb85a7565ff6ce7495", + "gen_hash_rust": "sha256:6eac58459c46d6cd03c0ec4863738bf57d2be61ea794c729ff1405e6c6e66dbf", + "gen_hash_verilog": "sha256:a4fdfa74eaa36dba431047d9ac95c10df830c734aa0e8fdf01c89d7c79102e13", + "gen_hash_zig": "sha256:5479058d720ae4e923921aa3d03d2ce5c3cea5b97204827c562d265b851dca52", + "module": "ClawRefresh", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T14:05:32Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:924b8dd3d4093d1315a6bbc6c769cc63a4074c1910bdd8a2cfa9ab605a65d458", + "spec_path": "specs/claw/refresh.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 44, + "total": 44, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 8c07378bd7..37061d846e 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -190,6 +190,7 @@ {"path": "specs/claw/github_surface.t27", "t27b": "pass"}, {"path": "specs/claw/preview.t27", "t27b": "pass"}, {"path": "specs/claw/recipes.t27", "t27b": "pass"}, + {"path": "specs/claw/refresh.t27", "t27b": "pass"}, {"path": "specs/claw/sarif.t27", "t27b": "pass"}, {"path": "specs/claw/sarif_feed.t27", "t27b": "pass"}, {"path": "specs/claw/sarif_pins.t27", "t27b": "pass"}, diff --git a/gen/c/claw/refresh.c b/gen/c/claw/refresh.c new file mode 100644 index 0000000000..cefe391f92 --- /dev/null +++ b/gen/c/claw/refresh.c @@ -0,0 +1,1344 @@ +/* ============================================================================ + Generated from t27 spec: ClawRefresh + DO NOT EDIT - generated by t27c gen-c + phi^2 + 1/phi^2 = 3 | TRINITY + ============================================================================ */ + +#include +#include +#include +#include +#define t27_assert(c, m) do { if (!(c)) { __builtin_trap(); } } while (0) +#define assert_eq(a, b) do { if ((a) != (b)) { __builtin_trap(); } } while (0) + +#ifndef CLAWREFRESH_H +#define CLAWREFRESH_H + +/* ------------------------------------------------------- + Constants + ------------------------------------------------------- */ + +#define MAX_PRS 512 +#define REFRESH_CAP 30 +#define NFIELDS 11 +#define D_NONE 0 +#define D_REFRESH 1 +#define D_CONFLICT 2 +#define D_HELD 3 +#define D_FORK 4 +#define D_DRAFT 5 +#define D_CURRENT 6 +#define D_UNKNOWN 7 +#define D_CAPPED 8 +#define D_BAD 9 +#define D_COUNT 10 +#define M_OK 0 +#define M_CONFLICT 1 +#define M_UNKNOWN 2 +#define T_FALSE 0 +#define T_TRUE 1 +#define T_BAD 2 +#define HELD_LABEL "no-auto-merge" +#define HELD_LABEL_LEN 13 +#define SUM_HEAD "M\tclaw-refresh: " +#define SUM_HEAD_LEN 16 +static const const char* SUM_TEXTS[8] = { " to update, ", " in conflict, ", " held or draft, ", " fork without maintainer edits, ", " already up to date, ", " not decidable yet, ", " over the cap, ", " unreadable\n" }; +static const size_t SUM_LENS[8] = { 12, 14, 16, 32, 21, 20, 15, 12 }; +#define CONFLICT_HEAD "M\t- #" +#define CONFLICT_HEAD_LEN 5 +#define CONFLICT_TAIL " conflicts with its base branch and was left alone\n" +#define CONFLICT_TAIL_LEN 51 +#define BLANK_LINE "M\t\n" +#define BLANK_LINE_LEN 3 + +/* ------------------------------------------------------- + Function prototypes + ------------------------------------------------------- */ + +uint8_t decide(bool held, bool draft, bool fork_blocked, uint8_t mc, int32_t bh); +bool rf_label_held(uint8_t* raw, size_t s, size_t e); +uint8_t rf_flag(uint8_t* raw, size_t s, size_t e); +uint8_t rf_merge_class(uint8_t* raw, size_t ms, size_t me, size_t ss, size_t se); +int32_t rf_behind(uint8_t* raw, size_t bs, size_t be, size_t ss, size_t se); +bool rf_hex(uint8_t b); +bool rf_digit(uint8_t b); +bool rf_sha_ok(uint8_t* raw, size_t s, size_t e); +bool rf_created_ok(uint8_t* raw, size_t s, size_t e); +bool rf_older(uint8_t* raw, size_t ca, uint32_t na, size_t cb, uint32_t nb); +bool rf_split(uint8_t* raw, size_t ls, size_t le, size_t* f); +uint8_t rf_classify(uint8_t* raw, size_t* f); +size_t rf_put_sha(uint8_t* dst, size_t cap, size_t w, uint8_t* raw, size_t s); +size_t refresh_plan(uint8_t* raw, size_t n, uint8_t* out, size_t cap); +bool refresh_fits(size_t needed, size_t out_cap); +size_t t_put2(uint8_t* dst, size_t cap, size_t w, uint32_t v); +size_t t_rec(uint8_t* dst, size_t cap, size_t w, uint32_t num, uint32_t ts, bool draft, uint32_t labels, bool cross, bool modify, uint32_t mg, uint32_t sc, uint32_t behind); +uint32_t t_lines(uint8_t* out, size_t n, uint8_t b); +int32_t digit_val(uint8_t b); +size_t field_end(uint8_t* text, size_t e, size_t s); +bool field_eq(uint8_t* text, size_t s, size_t e, const char* name, size_t n); +size_t line_end(uint8_t* text, size_t len, size_t s); +int32_t parse_num(uint8_t* text, size_t s, size_t e); +size_t put_byte(uint8_t* dst, size_t cap, size_t w, uint8_t b); +bool buf_has(uint8_t* buf, size_t buf_len, const char* needle, size_t needle_len); +uint8_t digit_byte(uint32_t d); +size_t put_count(uint8_t* dst, size_t cap, size_t w, uint32_t v); +size_t put_str(uint8_t* dst, size_t cap, size_t w, const char* s, size_t s_len); + +/* ------------------------------------------------------- + Function implementations + ------------------------------------------------------- */ + +uint8_t decide(bool held, bool draft, bool fork_blocked, uint8_t mc, int32_t bh) { + if (held) { + return D_HELD; + } + if (draft) { + return D_DRAFT; + } + if (fork_blocked) { + return D_FORK; + } + if ((mc == M_CONFLICT)) { + return D_CONFLICT; + } + if ((mc == M_UNKNOWN)) { + return D_UNKNOWN; + } + if ((bh < 0)) { + return D_UNKNOWN; + } + if ((bh == 0)) { + return D_CURRENT; + } + return D_REFRESH; +} + +bool rf_label_held(uint8_t* raw, size_t s, size_t e) { + size_t p = s; + while ((p <= e)) { + size_t q = p; + while (((q < e) && (raw[q] != ','))) { + q += 1; + } + if (field_eq(raw, p, q, HELD_LABEL, HELD_LABEL_LEN)) { + return true; + } + p = (q + 1); + } + return false; +} + +uint8_t rf_flag(uint8_t* raw, size_t s, size_t e) { + if (field_eq(raw, s, e, "true", 4)) { + return T_TRUE; + } + if (field_eq(raw, s, e, "false", 5)) { + return T_FALSE; + } + return T_BAD; +} + +uint8_t rf_merge_class(uint8_t* raw, size_t ms, size_t me, size_t ss, size_t se) { + if (field_eq(raw, ms, me, "CONFLICTING", 11)) { + return M_CONFLICT; + } + if (field_eq(raw, ss, se, "DIRTY", 5)) { + return M_CONFLICT; + } + if (field_eq(raw, ms, me, "MERGEABLE", 9)) { + return M_OK; + } + return M_UNKNOWN; +} + +int32_t rf_behind(uint8_t* raw, size_t bs, size_t be, size_t ss, size_t se) { + if (field_eq(raw, ss, se, "BEHIND", 6)) { + return 1; + } + int32_t n = parse_num(raw, bs, be); + if ((n > 0)) { + return 1; + } + if ((n == 0)) { + return 0; + } + return -1; +} + +bool rf_hex(uint8_t b) { + if (((b >= '0') && (b <= '9'))) { + return true; + } + return ((b >= 'a') && (b <= 'f')); +} + +bool rf_digit(uint8_t b) { + return ((b >= '0') && (b <= '9')); +} + +bool rf_sha_ok(uint8_t* raw, size_t s, size_t e) { + if (((e - s) != 40)) { + return false; + } + size_t bad = 0; + size_t i = s; + while ((i < e)) { + if ((rf_hex(raw[i]) == false)) { + bad += 1; + } + i += 1; + } + return (bad == 0); +} + +bool rf_created_ok(uint8_t* raw, size_t s, size_t e) { + if (((e - s) != 20)) { + return false; + } + size_t i = 0; + while ((i < 20)) { + uint8_t b = raw[(s + i)]; + if (((i == 4) || (i == 7))) { + if ((b != '-')) { + return false; + } + } else if ((i == 10)) { + if ((b != 'T')) { + return false; + } + } else if (((i == 13) || (i == 16))) { + if ((b != ':')) { + return false; + } + } else if ((i == 19)) { + if ((b != 'Z')) { + return false; + } + } else if ((rf_digit(b) == false)) { + return false; + } + i += 1; + } + return true; +} + +bool rf_older(uint8_t* raw, size_t ca, uint32_t na, size_t cb, uint32_t nb) { + size_t k = 0; + while ((k < 20)) { + uint8_t x = raw[(ca + k)]; + uint8_t y = raw[(cb + k)]; + if ((x < y)) { + return true; + } + if ((x > y)) { + return false; + } + k += 1; + } + return (na < nb); +} + +bool rf_split(uint8_t* raw, size_t ls, size_t le, size_t* f) { + size_t k = 0; + size_t s = ls; + while ((k < NFIELDS)) { + size_t e = field_end(raw, le, s); + f[(2 * k)] = s; + f[((2 * k) + 1)] = e; + k += 1; + if ((k < NFIELDS)) { + if ((e >= le)) { + return false; + } + s = (e + 1); + } else { + return (e == le); + } + } + return false; +} + +uint8_t rf_classify(uint8_t* raw, size_t* f) { + if ((rf_sha_ok(raw, f[2], f[3]) == false)) { + return D_BAD; + } + if ((rf_created_ok(raw, f[4], f[5]) == false)) { + return D_BAD; + } + uint8_t draft = rf_flag(raw, f[6], f[7]); + uint8_t cross = rf_flag(raw, f[10], f[11]); + uint8_t modify = rf_flag(raw, f[14], f[15]); + if ((((draft == T_BAD) || (cross == T_BAD)) || (modify == T_BAD))) { + return D_BAD; + } + bool held = rf_label_held(raw, f[8], f[9]); + bool gone = (f[13] == f[12]); + bool fork_blocked = (((cross == T_TRUE) || gone) && (modify != T_TRUE)); + uint8_t mc = rf_merge_class(raw, f[16], f[17], f[18], f[19]); + int32_t bh = rf_behind(raw, f[20], f[21], f[18], f[19]); + return decide(held, (draft == T_TRUE), fork_blocked, mc, bh); +} + +size_t rf_put_sha(uint8_t* dst, size_t cap, size_t w, uint8_t* raw, size_t s) { + size_t x = w; + size_t i = 0; + while ((i < 40)) { + x = put_byte(dst, cap, x, raw[(s + i)]); + i += 1; + } + return x; +} + +size_t refresh_plan(uint8_t* raw, size_t n, uint8_t* out, size_t cap) { + uint8_t dec[512] = {0}; + uint32_t num[512] = {0}; + size_t cs[512] = {0}; + size_t ss[512] = {0}; + uint32_t rk[512] = {0}; + size_t f[22] = {0}; + uint32_t st[10] = {0}; + size_t cnt = 0; + uint32_t extra = 0; + size_t ls = 0; + while ((ls < n)) { + size_t le = line_end(raw, n, ls); + if ((le > ls)) { + if ((cnt >= MAX_PRS)) { + extra += 1; + } else { + uint8_t d = D_BAD; + uint32_t v = 0; + cs[cnt] = 0; + ss[cnt] = 0; + if (rf_split(raw, ls, le, &f)) { + int32_t pn = parse_num(raw, f[0], f[1]); + if ((pn > 0)) { + v = ((uint32_t)(pn)); + d = rf_classify(raw, &f); + cs[cnt] = f[4]; + ss[cnt] = f[2]; + } + } + size_t j = 0; + while ((j < cnt)) { + if (((v != 0) && (num[j] == v))) { + d = D_BAD; + } + j += 1; + } + num[cnt] = v; + dec[cnt] = d; + cnt += 1; + } + } + size_t nx = le; + while (((nx < n) && (raw[nx] != '\n'))) { + nx += 1; + } + ls = (nx + 1); + } + size_t i = 0; + while ((i < cnt)) { + rk[i] = 0; + if ((dec[i] == D_REFRESH)) { + size_t j = 0; + while ((j < cnt)) { + if (((j != i) && (dec[j] == D_REFRESH))) { + if (rf_older(raw, cs[j], num[j], cs[i], num[i])) { + rk[i] += 1; + } + } + j += 1; + } + } + i += 1; + } + i = 0; + while ((i < cnt)) { + if (((dec[i] == D_REFRESH) && (rk[i] >= REFRESH_CAP))) { + dec[i] = D_CAPPED; + } + i += 1; + } + size_t c = 0; + while ((c < D_COUNT)) { + st[c] = 0; + c += 1; + } + i = 0; + while ((i < cnt)) { + st[((size_t)(dec[i]))] += 1; + i += 1; + } + st[((size_t)(D_BAD))] += extra; + size_t w = 0; + uint32_t r = 0; + while ((r < REFRESH_CAP)) { + i = 0; + while ((i < cnt)) { + if (((dec[i] == D_REFRESH) && (rk[i] == r))) { + w = put_str(out, cap, w, "R\t", 2); + w = put_count(out, cap, w, num[i]); + w = put_byte(out, cap, w, '\t'); + w = rf_put_sha(out, cap, w, raw, ss[i]); + w = put_byte(out, cap, w, '\n'); + } + i += 1; + } + r += 1; + } + w = put_str(out, cap, w, SUM_HEAD, SUM_HEAD_LEN); + w = put_count(out, cap, w, st[((size_t)(D_REFRESH))]); + w = put_str(out, cap, w, SUM_TEXTS[0], SUM_LENS[0]); + w = put_count(out, cap, w, st[((size_t)(D_CONFLICT))]); + w = put_str(out, cap, w, SUM_TEXTS[1], SUM_LENS[1]); + w = put_count(out, cap, w, (st[((size_t)(D_HELD))] + st[((size_t)(D_DRAFT))])); + w = put_str(out, cap, w, SUM_TEXTS[2], SUM_LENS[2]); + w = put_count(out, cap, w, st[((size_t)(D_FORK))]); + w = put_str(out, cap, w, SUM_TEXTS[3], SUM_LENS[3]); + w = put_count(out, cap, w, st[((size_t)(D_CURRENT))]); + w = put_str(out, cap, w, SUM_TEXTS[4], SUM_LENS[4]); + w = put_count(out, cap, w, st[((size_t)(D_UNKNOWN))]); + w = put_str(out, cap, w, SUM_TEXTS[5], SUM_LENS[5]); + w = put_count(out, cap, w, st[((size_t)(D_CAPPED))]); + w = put_str(out, cap, w, SUM_TEXTS[6], SUM_LENS[6]); + w = put_count(out, cap, w, st[((size_t)(D_BAD))]); + w = put_str(out, cap, w, SUM_TEXTS[7], SUM_LENS[7]); + if ((st[((size_t)(D_CONFLICT))] > 0)) { + w = put_str(out, cap, w, BLANK_LINE, BLANK_LINE_LEN); + i = 0; + while ((i < cnt)) { + if ((dec[i] == D_CONFLICT)) { + w = put_str(out, cap, w, CONFLICT_HEAD, CONFLICT_HEAD_LEN); + w = put_count(out, cap, w, num[i]); + w = put_str(out, cap, w, CONFLICT_TAIL, CONFLICT_TAIL_LEN); + } + i += 1; + } + } + return w; +} + +bool refresh_fits(size_t needed, size_t out_cap) { + return (needed <= out_cap); +} + +size_t t_put2(uint8_t* dst, size_t cap, size_t w, uint32_t v) { + size_t x = put_byte(dst, cap, w, digit_byte((v / 10))); + x = put_byte(dst, cap, x, digit_byte((v % 10))); + return x; +} + +size_t t_rec(uint8_t* dst, size_t cap, size_t w, uint32_t num, uint32_t ts, bool draft, uint32_t labels, bool cross, bool modify, uint32_t mg, uint32_t sc, uint32_t behind) { + size_t x = put_count(dst, cap, w, num); + x = put_byte(dst, cap, x, '\t'); + x = put_byte(dst, cap, x, digit_byte((num % 10))); + size_t k = 1; + while ((k < 40)) { + x = put_byte(dst, cap, x, 'a'); + k += 1; + } + x = put_str(dst, cap, x, "\t2026-10-18T", 12); + x = t_put2(dst, cap, x, (ts / 3600)); + x = put_byte(dst, cap, x, ':'); + x = t_put2(dst, cap, x, ((ts % 3600) / 60)); + x = put_byte(dst, cap, x, ':'); + x = t_put2(dst, cap, x, (ts % 60)); + x = put_str(dst, cap, x, "Z\t", 2); + if (draft) { + x = put_str(dst, cap, x, "true", 4); + } else { + x = put_str(dst, cap, x, "false", 5); + } + x = put_byte(dst, cap, x, '\t'); + if ((labels == 1)) { + x = put_str(dst, cap, x, "no-auto-merge", 13); + } + if ((labels == 2)) { + x = put_str(dst, cap, x, "bug,no-auto-merge,x", 19); + } + if ((labels == 3)) { + x = put_str(dst, cap, x, "no-auto-merge-2", 15); + } + if ((labels == 4)) { + x = put_str(dst, cap, x, "x-no-auto-merge", 15); + } + if ((labels == 5)) { + x = put_str(dst, cap, x, "bug,x", 5); + } + x = put_byte(dst, cap, x, '\t'); + if (cross) { + x = put_str(dst, cap, x, "true\tsomeone/t27\t", 17); + } else { + x = put_str(dst, cap, x, "false\tgHashTag/t27\t", 19); + } + if (modify) { + x = put_str(dst, cap, x, "true\t", 5); + } else { + x = put_str(dst, cap, x, "false\t", 6); + } + if ((mg == 0)) { + x = put_str(dst, cap, x, "MERGEABLE", 9); + } + if ((mg == 1)) { + x = put_str(dst, cap, x, "CONFLICTING", 11); + } + if ((mg == 2)) { + x = put_str(dst, cap, x, "UNKNOWN", 7); + } + x = put_byte(dst, cap, x, '\t'); + if ((sc == 0)) { + x = put_str(dst, cap, x, "BLOCKED", 7); + } + if ((sc == 1)) { + x = put_str(dst, cap, x, "DIRTY", 5); + } + if ((sc == 2)) { + x = put_str(dst, cap, x, "BEHIND", 6); + } + if ((sc == 3)) { + x = put_str(dst, cap, x, "UNKNOWN", 7); + } + x = put_byte(dst, cap, x, '\t'); + if ((behind == 9999)) { + x = put_byte(dst, cap, x, '-'); + } else { + x = put_count(dst, cap, x, behind); + } + return put_byte(dst, cap, x, '\n'); +} + +uint32_t t_lines(uint8_t* out, size_t n, uint8_t b) { + uint32_t c = 0; + size_t i = 0; + while ((i < n)) { + if (((out[i] == b) && ((i == 0) || (out[(i - 1)] == '\n')))) { + c += 1; + } + i += 1; + } + return c; +} + +int32_t digit_val(uint8_t b) { + if ((b == '0')) { + return 0; + } + if ((b == '1')) { + return 1; + } + if ((b == '2')) { + return 2; + } + if ((b == '3')) { + return 3; + } + if ((b == '4')) { + return 4; + } + if ((b == '5')) { + return 5; + } + if ((b == '6')) { + return 6; + } + if ((b == '7')) { + return 7; + } + if ((b == '8')) { + return 8; + } + if ((b == '9')) { + return 9; + } + return -1; +} + +size_t field_end(uint8_t* text, size_t e, size_t s) { + size_t i = s; + while ((i < e)) { + if ((text[i] == '\t')) { + return i; + } + i += 1; + } + return e; +} + +bool field_eq(uint8_t* text, size_t s, size_t e, const char* name, size_t n) { + if ((e < s)) { + return false; + } + if (((e - s) != n)) { + return false; + } + size_t i = 0; + while ((i < n)) { + if ((text[(s + i)] != name[i])) { + return false; + } + i += 1; + } + return true; +} + +size_t line_end(uint8_t* text, size_t len, size_t s) { + size_t i = s; + while ((i < len)) { + if ((text[i] == '\n')) { +break; + } + i += 1; + } + if ((i > s)) { + if ((text[(i - 1)] == 13)) { + return (i - 1); + } + } + return i; +} + +int32_t parse_num(uint8_t* text, size_t s, size_t e) { + if ((e <= s)) { + return -1; + } + if (((e - s) > 9)) { + return -1; + } + int32_t v = 0; + size_t i = s; + while ((i < e)) { + int32_t d = digit_val(text[i]); + if ((d < 0)) { + return -1; + } + v = ((v * 10) + d); + i += 1; + } + return v; +} + +size_t put_byte(uint8_t* dst, size_t cap, size_t w, uint8_t b) { + if ((w < cap)) { + dst[w] = b; + } + return (w + 1); +} + +bool buf_has(uint8_t* buf, size_t buf_len, const char* needle, size_t needle_len) { + if ((needle_len > buf_len)) { + return false; + } + size_t start = 0; + while (((start + needle_len) <= buf_len)) { + size_t k = 0; + size_t diff = 0; + while ((k < needle_len)) { + if ((buf[(start + k)] != needle[k])) { + diff += 1; + } + k += 1; + } + if ((diff == 0)) { + return true; + } + start += 1; + } + return false; +} + +uint8_t digit_byte(uint32_t d) { + uint8_t b = '0'; + if ((d == 1)) { + b = '1'; + } + if ((d == 2)) { + b = '2'; + } + if ((d == 3)) { + b = '3'; + } + if ((d == 4)) { + b = '4'; + } + if ((d == 5)) { + b = '5'; + } + if ((d == 6)) { + b = '6'; + } + if ((d == 7)) { + b = '7'; + } + if ((d == 8)) { + b = '8'; + } + if ((d == 9)) { + b = '9'; + } + return b; +} + +size_t put_count(uint8_t* dst, size_t cap, size_t w, uint32_t v) { + uint8_t tmp[16] = {0}; + size_t n = 0; + uint32_t x = v; + if ((x == 0)) { + tmp[0] = digit_byte(0); + n = 1; + } + while ((x > 0)) { + tmp[n] = digit_byte((x % 10)); + n += 1; + x = (x / 10); + } + size_t k = 0; + while ((k < n)) { + if (((w + k) < cap)) { + dst[(w + k)] = tmp[((n - 1) - k)]; + } + k += 1; + } + return (w + n); +} + +size_t put_str(uint8_t* dst, size_t cap, size_t w, const char* s, size_t s_len) { + size_t i = 0; + while ((i < s_len)) { + if (((w + i) < cap)) { + dst[(w + i)] = s[i]; + } + i += 1; + } + return (w + s_len); +} + +/* ------------------------------------------------------- + Tests + ------------------------------------------------------- */ + +void test_a_behind_conflict_free_pull_request_is_refreshed(void) { + assert((decide(false, false, false, M_OK, 3) == D_REFRESH)); + assert((decide(false, false, false, M_OK, 1) == D_REFRESH)); +} + +void test_a_pull_request_that_is_not_behind_is_left_alone(void) { + assert((decide(false, false, false, M_OK, 0) == D_CURRENT)); +} + +void test_a_conflicting_pull_request_is_never_refreshed(void) { + assert((decide(false, false, false, M_CONFLICT, 5) == D_CONFLICT)); + assert((decide(false, false, false, M_CONFLICT, 0) == D_CONFLICT)); +} + +void test_a_held_pull_request_is_never_touched_whatever_else_is_true(void) { + assert((decide(true, false, false, M_OK, 5) == D_HELD)); + assert((decide(true, false, false, M_CONFLICT, 5) == D_HELD)); + assert((decide(true, true, true, M_UNKNOWN, 0) == D_HELD)); +} + +void test_a_draft_is_never_touched(void) { + assert((decide(false, true, false, M_OK, 5) == D_DRAFT)); + assert((decide(false, true, false, M_CONFLICT, 5) == D_DRAFT)); +} + +void test_a_fork_without_maintainer_edits_is_skipped_not_failed(void) { + assert((decide(false, false, true, M_OK, 5) == D_FORK)); + assert((decide(false, false, true, M_CONFLICT, 5) == D_FORK)); +} + +void test_a_fork_with_maintainer_edits_is_refreshed_like_any_other(void) { + assert((decide(false, false, false, M_OK, 5) == D_REFRESH)); +} + +void test_unread_facts_never_become_a_refresh(void) { + assert((decide(false, false, false, M_UNKNOWN, 5) == D_UNKNOWN)); + assert((decide(false, false, false, M_OK, -1) == D_UNKNOWN)); +} + +void test_the_held_label_matches_only_as_a_whole_entry(void) { + uint8_t b[64] = {0}; + size_t n = put_str(&b, 64, 0, "no-auto-merge", 13); + assert((rf_label_held(&b, 0, n) == true)); + n = put_str(&b, 64, 0, "bug,no-auto-merge,x", 19); + assert((rf_label_held(&b, 0, n) == true)); + n = put_str(&b, 64, 0, "bug,no-auto-merge", 17); + assert((rf_label_held(&b, 0, n) == true)); + n = put_str(&b, 64, 0, "no-auto-merge-2", 15); + assert((rf_label_held(&b, 0, n) == false)); + n = put_str(&b, 64, 0, "x-no-auto-merge", 15); + assert((rf_label_held(&b, 0, n) == false)); + n = put_str(&b, 64, 0, "no-auto-merg", 12); + assert((rf_label_held(&b, 0, n) == false)); + n = put_str(&b, 64, 0, "bug,x", 5); + assert((rf_label_held(&b, 0, n) == false)); + assert((rf_label_held(&b, 0, 0) == false)); +} + +void test_a_flag_is_true_false_or_refused(void) { + uint8_t b[16] = {0}; + size_t n = put_str(&b, 16, 0, "true", 4); + assert((rf_flag(&b, 0, n) == T_TRUE)); + n = put_str(&b, 16, 0, "false", 5); + assert((rf_flag(&b, 0, n) == T_FALSE)); + n = put_str(&b, 16, 0, "True", 4); + assert((rf_flag(&b, 0, n) == T_BAD)); + n = put_str(&b, 16, 0, "", 0); + assert((rf_flag(&b, 0, n) == T_BAD)); + n = put_str(&b, 16, 0, "1", 1); + assert((rf_flag(&b, 0, n) == T_BAD)); +} + +void test_mergeability_folds_conflict_and_unknown_apart(void) { + uint8_t b[64] = {0}; + size_t m = put_str(&b, 64, 0, "MERGEABLE", 9); + size_t s = put_str(&b, 64, m, "BLOCKED", 7); + assert((rf_merge_class(&b, 0, m, m, s) == M_OK)); + s = put_str(&b, 64, m, "DIRTY", 5); + assert((rf_merge_class(&b, 0, m, m, s) == M_CONFLICT)); + m = put_str(&b, 64, 0, "CONFLICTING", 11); + s = put_str(&b, 64, m, "BLOCKED", 7); + assert((rf_merge_class(&b, 0, m, m, s) == M_CONFLICT)); + m = put_str(&b, 64, 0, "UNKNOWN", 7); + s = put_str(&b, 64, m, "UNKNOWN", 7); + assert((rf_merge_class(&b, 0, m, m, s) == M_UNKNOWN)); + m = put_str(&b, 64, 0, "WHATEVER", 8); + s = put_str(&b, 64, m, "CLEAN", 5); + assert((rf_merge_class(&b, 0, m, m, s) == M_UNKNOWN)); +} + +void test_behind_reads_the_state_word_then_the_count(void) { + uint8_t b[64] = {0}; + size_t c = put_str(&b, 64, 0, "4", 1); + size_t s = put_str(&b, 64, c, "BLOCKED", 7); + assert((rf_behind(&b, 0, c, c, s) == 1)); + c = put_str(&b, 64, 0, "0", 1); + s = put_str(&b, 64, c, "BLOCKED", 7); + assert((rf_behind(&b, 0, c, c, s) == 0)); + s = put_str(&b, 64, c, "BEHIND", 6); + assert((rf_behind(&b, 0, c, c, s) == 1)); + c = put_str(&b, 64, 0, "-", 1); + s = put_str(&b, 64, c, "BLOCKED", 7); + assert((rf_behind(&b, 0, c, c, s) == -1)); + s = put_str(&b, 64, c, "BEHIND", 6); + assert((rf_behind(&b, 0, c, c, s) == 1)); +} + +void test_a_sha_is_forty_lowercase_hex_digits(void) { + uint8_t b[64] = {0}; + size_t n = 0; + while ((n < 40)) { + b[n] = 'a'; + n += 1; + } + assert((rf_sha_ok(&b, 0, 40) == true)); + assert((rf_sha_ok(&b, 0, 39) == false)); + b[7] = 'G'; + assert((rf_sha_ok(&b, 0, 40) == false)); + b[7] = 'A'; + assert((rf_sha_ok(&b, 0, 40) == false)); + b[7] = '9'; + assert((rf_sha_ok(&b, 0, 40) == true)); + b[7] = 'f'; + assert((rf_sha_ok(&b, 0, 40) == true)); + b[7] = 'g'; + assert((rf_sha_ok(&b, 0, 40) == false)); +} + +void test_a_created_stamp_has_exactly_the_sortable_shape(void) { + uint8_t b[32] = {0}; + size_t n = put_str(&b, 32, 0, "2026-10-18T09:05:01Z", 20); + assert((rf_created_ok(&b, 0, n) == true)); + n = put_str(&b, 32, 0, "2026-10-18 09:05:01Z", 20); + assert((rf_created_ok(&b, 0, n) == false)); + n = put_str(&b, 32, 0, "2026-10-18T09:05:01+0", 20); + assert((rf_created_ok(&b, 0, n) == false)); + n = put_str(&b, 32, 0, "2026-10-18T09:05:01", 19); + assert((rf_created_ok(&b, 0, n) == false)); + n = put_str(&b, 32, 0, "2026-1x-18T09:05:01Z", 20); + assert((rf_created_ok(&b, 0, n) == false)); +} + +void test_older_goes_by_time_then_by_number(void) { + uint8_t b[64] = {0}; + size_t a = put_str(&b, 64, 0, "2026-10-18T09:05:01Z", 20); + size_t c = put_str(&b, 64, a, "2026-10-18T09:05:02Z", 20); + assert((rf_older(&b, 0, 900, a, 100) == true)); + assert((rf_older(&b, a, 100, 0, 900) == false)); + assert((rf_older(&b, 0, 5, 0, 6) == true)); + assert((rf_older(&b, 0, 6, 0, 5) == false)); + assert((rf_older(&b, 0, 5, 0, 5) == false)); + assert((c == 40)); +} + +void test_a_behind_clean_pull_request_gets_an_R_line_with_its_sha(void) { + uint8_t r[256] = {0}; + size_t n = t_rec(&r, 256, 0, 4217, 600, false, 0, false, false, 0, 0, 3); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((refresh_fits(d, 4096) == true)); + assert((out[0] == 'R')); + assert((out[1] == '\t')); + assert((buf_has(&out, d, "R\t4217\t7aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", 48) == true)); + assert((t_lines(&out, d, 'R') == 1)); + assert((buf_has(&out, d, "M\tclaw-refresh: 1 to update, 0 in conflict, 0 held or draft, 0 fork without maintainer edits, 0 already up to date, 0 not decidable yet, 0 over the cap, 0 unreadable\n", 166) == true)); +} + +void test_a_conflicting_pull_request_is_listed_once_and_not_refreshed(void) { + uint8_t r[512] = {0}; + size_t n = t_rec(&r, 512, 0, 300, 100, false, 0, false, false, 1, 1, 7); + n = t_rec(&r, 512, n, 301, 200, false, 0, false, false, 0, 0, 2); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 1)); + assert((buf_has(&out, d, "R\t301\t", 6) == true)); + assert((buf_has(&out, d, "R\t300\t", 6) == false)); + assert((buf_has(&out, d, "M\t- #300 conflicts with its base branch and was left alone\n", 59) == true)); + assert((buf_has(&out, d, "M\t- #301", 8) == false)); + assert((buf_has(&out, d, " 1 in conflict, ", 16) == true)); +} + +void test_a_dirty_state_word_alone_is_a_conflict(void) { + uint8_t r[256] = {0}; + size_t n = t_rec(&r, 256, 0, 310, 100, false, 0, false, false, 0, 1, 4); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 0)); + assert((buf_has(&out, d, "M\t- #310 ", 9) == true)); +} + +void test_a_draft_and_a_held_pull_request_are_not_touched(void) { + uint8_t r[1024] = {0}; + size_t n = t_rec(&r, 1024, 0, 400, 100, true, 0, false, false, 0, 0, 9); + n = t_rec(&r, 1024, n, 401, 110, false, 1, false, false, 0, 0, 9); + n = t_rec(&r, 1024, n, 402, 120, false, 2, false, false, 0, 0, 9); + n = t_rec(&r, 1024, n, 403, 130, false, 3, false, false, 0, 0, 9); + n = t_rec(&r, 1024, n, 404, 140, false, 4, false, false, 0, 0, 9); + n = t_rec(&r, 1024, n, 405, 150, false, 5, false, false, 0, 0, 9); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((buf_has(&out, d, "R\t400\t", 6) == false)); + assert((buf_has(&out, d, "R\t401\t", 6) == false)); + assert((buf_has(&out, d, "R\t402\t", 6) == false)); + assert((buf_has(&out, d, "R\t403\t", 6) == true)); + assert((buf_has(&out, d, "R\t404\t", 6) == true)); + assert((buf_has(&out, d, "R\t405\t", 6) == true)); + assert((t_lines(&out, d, 'R') == 3)); + assert((buf_has(&out, d, " 3 held or draft, ", 18) == true)); +} + +void test_a_fork_without_maintainer_edits_is_skipped_and_one_with_edits_is_refreshed(void) { + uint8_t r[1024] = {0}; + size_t n = t_rec(&r, 1024, 0, 500, 100, false, 0, true, false, 0, 0, 9); + n = t_rec(&r, 1024, n, 501, 110, false, 0, true, true, 0, 0, 9); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((buf_has(&out, d, "R\t500\t", 6) == false)); + assert((buf_has(&out, d, "R\t501\t", 6) == true)); + assert((t_lines(&out, d, 'R') == 1)); + assert((buf_has(&out, d, " 1 fork without maintainer edits, ", 34) == true)); +} + +void test_a_deleted_head_repository_counts_as_a_fork_that_cannot_be_edited(void) { + uint8_t r[256] = {0}; + size_t n = put_str(&r, 256, 0, "510\t", 4); + size_t k = 0; + while ((k < 40)) { + n = put_byte(&r, 256, n, 'b'); + k += 1; + } + n = put_str(&r, 256, n, "\t2026-10-18T00:01:00Z\tfalse\t\tfalse\t\tfalse\tMERGEABLE\tBLOCKED\t5\n", 62); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 0)); + assert((buf_has(&out, d, " 1 fork without maintainer edits, ", 34) == true)); +} + +void test_a_pull_request_that_is_current_or_not_computed_is_counted_and_left_alone(void) { + uint8_t r[1024] = {0}; + size_t n = t_rec(&r, 1024, 0, 600, 100, false, 0, false, false, 0, 0, 0); + n = t_rec(&r, 1024, n, 601, 110, false, 0, false, false, 2, 3, 5); + n = t_rec(&r, 1024, n, 602, 120, false, 0, false, false, 0, 0, 9999); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 0)); + assert((buf_has(&out, d, " 1 already up to date, ", 23) == true)); + assert((buf_has(&out, d, " 2 not decidable yet, ", 22) == true)); +} + +void test_a_record_that_cannot_be_read_is_counted_and_never_refreshed(void) { + uint8_t r[2048] = {0}; + size_t n = put_str(&r, 2048, 0, "700\tnotasha\t2026-10-18T00:01:00Z\tfalse\t\tfalse\tx/y\tfalse\tMERGEABLE\tBLOCKED\t5\n", 76); + n = put_str(&r, 2048, n, "701\tshort record\n", 17); + n = put_str(&r, 2048, n, "abc\tdef\n", 8); + n = put_str(&r, 2048, n, "0\t", 2); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 0)); + assert((buf_has(&out, d, " 4 unreadable\n", 14) == true)); +} + +void test_a_flag_that_is_not_true_or_false_makes_the_record_unreadable(void) { + uint8_t r[512] = {0}; + size_t n = t_rec(&r, 512, 0, 710, 100, false, 0, false, false, 0, 0, 5); + size_t k = 0; + while ((k < n)) { + if ((((((r[k] == 'f') && (r[(k + 1)] == 'a')) && (r[(k + 2)] == 'l')) && (r[(k + 3)] == 's')) && (r[(k + 4)] == 'e'))) { + r[k] = 'F'; + k = n; + } + k += 1; + } + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 0)); + assert((buf_has(&out, d, " 1 unreadable\n", 14) == true)); +} + +void test_a_number_seen_twice_is_refused_the_second_time(void) { + uint8_t r[1024] = {0}; + size_t n = t_rec(&r, 1024, 0, 800, 100, false, 0, false, false, 0, 0, 5); + n = t_rec(&r, 1024, n, 800, 200, false, 0, false, false, 0, 0, 5); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 1)); + assert((buf_has(&out, d, " 1 unreadable\n", 14) == true)); +} + +void test_the_oldest_pull_request_comes_first_whatever_the_input_order(void) { + uint8_t r[1024] = {0}; + size_t n = t_rec(&r, 1024, 0, 903, 3000, false, 0, false, false, 0, 0, 1); + n = t_rec(&r, 1024, n, 901, 1000, false, 0, false, false, 0, 0, 1); + n = t_rec(&r, 1024, n, 902, 2000, false, 0, false, false, 0, 0, 1); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 3)); + assert((out[0] == 'R')); + assert((buf_has(&out, d, "R\t901\t", 6) == true)); + assert((out[2] == '9')); + assert((out[3] == '0')); + assert((out[4] == '1')); + assert((out[47] == 'R')); + assert((out[49] == '9')); + assert((out[50] == '0')); + assert((out[51] == '2')); + assert((out[94] == 'R')); + assert((out[98] == '3')); +} + +void test_equal_timestamps_are_ordered_by_number(void) { + uint8_t r[1024] = {0}; + size_t n = t_rec(&r, 1024, 0, 952, 500, false, 0, false, false, 0, 0, 1); + n = t_rec(&r, 1024, n, 951, 500, false, 0, false, false, 0, 0, 1); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((out[4] == '1')); + assert((out[5] == '\t')); +} + +void test_at_most_thirty_are_refreshed_and_they_are_the_oldest(void) { + uint8_t r[8192] = {0}; + size_t n = 0; + uint32_t i = 35; + while ((i > 0)) { + n = t_rec(&r, 8192, n, (100 + i), (i * 60), false, 0, false, false, 0, 0, 2); + i -= 1; + } + uint8_t out[16384] = {0}; + size_t d = refresh_plan(&r, n, &out, 16384); + assert((refresh_fits(d, 16384) == true)); + assert((t_lines(&out, d, 'R') == 30)); + assert((buf_has(&out, d, "R\t101\t", 6) == true)); + assert((buf_has(&out, d, "R\t130\t", 6) == true)); + assert((buf_has(&out, d, "R\t131\t", 6) == false)); + assert((buf_has(&out, d, "R\t135\t", 6) == false)); + assert((buf_has(&out, d, " 30 to update, ", 15) == true)); + assert((buf_has(&out, d, " 5 over the cap, ", 17) == true)); +} + +void test_thirty_exactly_is_not_capped(void) { + uint8_t r[8192] = {0}; + size_t n = 0; + uint32_t i = 1; + while ((i <= 30)) { + n = t_rec(&r, 8192, n, (200 + i), (i * 60), false, 0, false, false, 0, 0, 2); + i += 1; + } + uint8_t out[16384] = {0}; + size_t d = refresh_plan(&r, n, &out, 16384); + assert((t_lines(&out, d, 'R') == 30)); + assert((buf_has(&out, d, " 0 over the cap, ", 17) == true)); +} + +void test_held_and_conflicting_ones_do_not_use_up_the_cap(void) { + uint8_t r[16384] = {0}; + size_t n = 0; + uint32_t i = 1; + while ((i <= 10)) { + n = t_rec(&r, 16384, n, (300 + i), (i * 60), false, 1, false, false, 0, 0, 2); + i += 1; + } + i = 11; + while ((i <= 20)) { + n = t_rec(&r, 16384, n, (300 + i), (i * 60), false, 0, false, false, 1, 1, 2); + i += 1; + } + i = 21; + while ((i <= 50)) { + n = t_rec(&r, 16384, n, (300 + i), (i * 60), false, 0, false, false, 0, 0, 2); + i += 1; + } + uint8_t out[16384] = {0}; + size_t d = refresh_plan(&r, n, &out, 16384); + assert((t_lines(&out, d, 'R') == 30)); + assert((buf_has(&out, d, "R\t321\t", 6) == true)); + assert((buf_has(&out, d, "R\t350\t", 6) == true)); + assert((t_lines(&out, d, 'M') == 12)); +} + +void test_an_empty_list_is_a_valid_empty_answer(void) { + uint8_t r[4] = {0}; + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, 0, &out, 4096); + assert((t_lines(&out, d, 'R') == 0)); + assert((buf_has(&out, d, "M\tclaw-refresh: 0 to update, 0 in conflict, 0 held or draft, 0 fork without maintainer edits, 0 already up to date, 0 not decidable yet, 0 over the cap, 0 unreadable\n", 166) == true)); + assert((t_lines(&out, d, 'M') == 1)); +} + +void test_a_carriage_return_does_not_hide_a_record(void) { + uint8_t r[512] = {0}; + size_t n = t_rec(&r, 512, 0, 970, 100, false, 0, false, false, 0, 0, 2); + r[(n - 1)] = 13; + n = put_byte(&r, 512, n, '\n'); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 1)); +} + +void test_a_buffer_too_small_is_reported_not_trusted(void) { + uint8_t r[256] = {0}; + size_t n = t_rec(&r, 256, 0, 980, 100, false, 0, false, false, 0, 0, 2); + uint8_t out[32] = {0}; + size_t d = refresh_plan(&r, n, &out, 32); + assert((refresh_fits(d, 32) == false)); + assert((refresh_fits(d, d) == true)); +} + +void test_the_summary_texts_are_pinned_by_their_lengths(void) { + uint8_t b[64] = {0}; + size_t k = 0; + while ((k < 8)) { + size_t w = put_str(&b, 64, 0, SUM_TEXTS[k], SUM_LENS[k]); + assert((w == SUM_LENS[k])); + assert((b[0] == ' ')); + k += 1; + } + size_t w = put_str(&b, 64, 0, SUM_TEXTS[7], SUM_LENS[7]); + assert((b[(w - 1)] == '\n')); + assert((b[(w - 2)] == 'e')); + w = put_str(&b, 64, 0, CONFLICT_TAIL, CONFLICT_TAIL_LEN); + assert((b[(w - 1)] == '\n')); + assert((b[(w - 2)] == 'e')); + w = put_str(&b, 64, 0, SUM_HEAD, SUM_HEAD_LEN); + assert((b[(w - 1)] == ' ')); + assert((b[(w - 2)] == ':')); + w = put_str(&b, 64, 0, CONFLICT_HEAD, CONFLICT_HEAD_LEN); + assert((b[(w - 1)] == '#')); + w = put_str(&b, 64, 0, BLANK_LINE, BLANK_LINE_LEN); + assert((b[(w - 1)] == '\n')); + assert((b[(w - 2)] == '\t')); +} + +void test_one_record_splits_into_eleven_fields_and_classifies_as_refresh(void) { + uint8_t r[256] = {0}; + size_t n = t_rec(&r, 256, 0, 4217, 600, false, 0, false, false, 0, 0, 3); + size_t f[22] = {0}; + size_t le = line_end(&r, n, 0); + assert((le == (n - 1))); + assert((rf_split(&r, 0, le, &f) == true)); + assert((f[1] == 4)); + assert((parse_num(&r, f[0], f[1]) == 4217)); + assert((rf_sha_ok(&r, f[2], f[3]) == true)); + assert((rf_created_ok(&r, f[4], f[5]) == true)); + assert((rf_flag(&r, f[6], f[7]) == T_FALSE)); + assert((rf_flag(&r, f[10], f[11]) == T_FALSE)); + assert((rf_flag(&r, f[14], f[15]) == T_FALSE)); + assert((rf_merge_class(&r, f[16], f[17], f[18], f[19]) == M_OK)); + assert((rf_behind(&r, f[20], f[21], f[18], f[19]) == 1)); + assert((rf_classify(&r, &f) == D_REFRESH)); +} + +void test_a_created_stamp_is_checked_at_every_separator_and_at_its_length(void) { + uint8_t b[32] = {0}; + size_t n = put_str(&b, 32, 0, "2026-10-18T09:05:01Zx", 21); + assert((rf_created_ok(&b, 0, n) == false)); + n = put_str(&b, 32, 0, "2026/10-18T09:05:01Z", 20); + assert((rf_created_ok(&b, 0, n) == false)); + n = put_str(&b, 32, 0, "2026-10/18T09:05:01Z", 20); + assert((rf_created_ok(&b, 0, n) == false)); + n = put_str(&b, 32, 0, "2026-10-18T09-05:01Z", 20); + assert((rf_created_ok(&b, 0, n) == false)); + n = put_str(&b, 32, 0, "2026-10-18T09:05-01Z", 20); + assert((rf_created_ok(&b, 0, n) == false)); +} + +void test_older_ignores_the_byte_after_the_stamp(void) { + uint8_t b[64] = {0}; + size_t w = put_str(&b, 64, 0, "2026-10-18T09:05:01Zz", 21); + w = put_str(&b, 64, w, "2026-10-18T09:05:01Za", 21); + assert((rf_older(&b, 0, 1, 21, 2) == true)); + assert((rf_older(&b, 21, 2, 0, 1) == false)); +} + +void test_one_unreadable_field_alone_makes_the_record_unreadable(void) { + uint8_t r[256] = {0}; + size_t n = t_rec(&r, 256, 0, 720, 100, false, 0, false, false, 0, 0, 5); + assert((r[45] == '2')); + assert((r[73] == 'f')); + assert((r[92] == 'f')); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 1)); + r[45] = 'x'; + d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 0)); + assert((buf_has(&out, d, " 1 unreadable\n", 14) == true)); + r[45] = '2'; + r[73] = 'F'; + d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 0)); + assert((buf_has(&out, d, " 1 unreadable\n", 14) == true)); + r[73] = 'f'; + r[92] = 'F'; + d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 0)); + assert((buf_has(&out, d, " 1 unreadable\n", 14) == true)); +} + +void test_a_record_one_field_short_is_unreadable_not_merely_undecidable(void) { + uint8_t r[256] = {0}; + size_t n = t_rec(&r, 256, 0, 730, 100, false, 0, false, false, 0, 0, 5); + r[(n - 3)] = '\n'; + n -= 2; + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 0)); + assert((buf_has(&out, d, " 1 unreadable\n", 14) == true)); + assert((buf_has(&out, d, " 0 not decidable yet, ", 22) == true)); +} + +void test_a_blank_line_is_not_a_record(void) { + uint8_t r[512] = {0}; + size_t n = put_byte(&r, 512, 0, '\n'); + n = t_rec(&r, 512, n, 740, 100, false, 0, false, false, 0, 0, 5); + n = put_byte(&r, 512, n, '\n'); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 1)); + assert((buf_has(&out, d, " 0 unreadable\n", 14) == true)); +} + +void test_pull_request_number_zero_is_unreadable(void) { + uint8_t r[256] = {0}; + size_t n = t_rec(&r, 256, 0, 0, 100, false, 0, false, false, 0, 0, 5); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 0)); + assert((buf_has(&out, d, " 1 unreadable\n", 14) == true)); +} + +void test_the_513th_record_is_counted_as_unreadable_and_not_stored(void) { + uint8_t r[65536] = {0}; + size_t n = 0; + uint32_t i = 1; + while ((i <= 513)) { + n = t_rec(&r, 65536, n, i, 100, false, 0, false, false, 0, 0, 0); + i += 1; + } + assert((n < 65536)); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((buf_has(&out, d, "512 already up to date, ", 24) == true)); + assert((buf_has(&out, d, " 1 unreadable\n", 14) == true)); +} + +void test_the_hour_digits_decide_the_order(void) { + uint8_t r[1024] = {0}; + size_t n = t_rec(&r, 1024, 0, 9, 39600, false, 0, false, false, 0, 0, 1); + n = t_rec(&r, 1024, n, 3, 43200, false, 0, false, false, 0, 0, 1); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 2)); + assert((out[2] == '9')); + assert((out[47] == '3')); +} + +void test_the_second_digits_decide_the_order(void) { + uint8_t r[1024] = {0}; + size_t n = t_rec(&r, 1024, 0, 9, 3610, false, 0, false, false, 0, 0, 1); + n = t_rec(&r, 1024, n, 3, 3659, false, 0, false, false, 0, 0, 1); + uint8_t out[4096] = {0}; + size_t d = refresh_plan(&r, n, &out, 4096); + assert((t_lines(&out, d, 'R') == 2)); + assert((out[2] == '9')); + assert((out[47] == '3')); +} + + +/* ------------------------------------------------------- + Test runner (compile with -DT27_TEST_MAIN to execute) + ------------------------------------------------------- */ + +#ifdef T27_TEST_MAIN +#include +int main(void) { + test_a_behind_conflict_free_pull_request_is_refreshed(); + test_a_pull_request_that_is_not_behind_is_left_alone(); + test_a_conflicting_pull_request_is_never_refreshed(); + test_a_held_pull_request_is_never_touched_whatever_else_is_true(); + test_a_draft_is_never_touched(); + test_a_fork_without_maintainer_edits_is_skipped_not_failed(); + test_a_fork_with_maintainer_edits_is_refreshed_like_any_other(); + test_unread_facts_never_become_a_refresh(); + test_the_held_label_matches_only_as_a_whole_entry(); + test_a_flag_is_true_false_or_refused(); + test_mergeability_folds_conflict_and_unknown_apart(); + test_behind_reads_the_state_word_then_the_count(); + test_a_sha_is_forty_lowercase_hex_digits(); + test_a_created_stamp_has_exactly_the_sortable_shape(); + test_older_goes_by_time_then_by_number(); + test_a_behind_clean_pull_request_gets_an_R_line_with_its_sha(); + test_a_conflicting_pull_request_is_listed_once_and_not_refreshed(); + test_a_dirty_state_word_alone_is_a_conflict(); + test_a_draft_and_a_held_pull_request_are_not_touched(); + test_a_fork_without_maintainer_edits_is_skipped_and_one_with_edits_is_refreshed(); + test_a_deleted_head_repository_counts_as_a_fork_that_cannot_be_edited(); + test_a_pull_request_that_is_current_or_not_computed_is_counted_and_left_alone(); + test_a_record_that_cannot_be_read_is_counted_and_never_refreshed(); + test_a_flag_that_is_not_true_or_false_makes_the_record_unreadable(); + test_a_number_seen_twice_is_refused_the_second_time(); + test_the_oldest_pull_request_comes_first_whatever_the_input_order(); + test_equal_timestamps_are_ordered_by_number(); + test_at_most_thirty_are_refreshed_and_they_are_the_oldest(); + test_thirty_exactly_is_not_capped(); + test_held_and_conflicting_ones_do_not_use_up_the_cap(); + test_an_empty_list_is_a_valid_empty_answer(); + test_a_carriage_return_does_not_hide_a_record(); + test_a_buffer_too_small_is_reported_not_trusted(); + test_the_summary_texts_are_pinned_by_their_lengths(); + test_one_record_splits_into_eleven_fields_and_classifies_as_refresh(); + test_a_created_stamp_is_checked_at_every_separator_and_at_its_length(); + test_older_ignores_the_byte_after_the_stamp(); + test_one_unreadable_field_alone_makes_the_record_unreadable(); + test_a_record_one_field_short_is_unreadable_not_merely_undecidable(); + test_a_blank_line_is_not_a_record(); + test_pull_request_number_zero_is_unreadable(); + test_the_513th_record_is_counted_as_unreadable_and_not_stored(); + test_the_hour_digits_decide_the_order(); + test_the_second_digits_decide_the_order(); + printf("All %d tests passed.\n", 44); + return 0; +} +#endif /* T27_TEST_MAIN */ + +#endif /* CLAWREFRESH_H */ diff --git a/scripts/gen_w658.py b/scripts/gen_w658.py deleted file mode 100644 index 566cd48628..0000000000 --- a/scripts/gen_w658.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python3 -"""Generate specs/scratch/w658_bench_module_135x2p6_aos_var_call_write.t27.""" - -import os - -REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DST = os.path.join(REPO, "specs/scratch/w658_bench_module_135x2p6_aos_var_call_write.t27") - -OUTER = 135 -TOTAL = OUTER * 2 ** 6 -LAST_IDX = OUTER - 1 -MID_IDX = OUTER // 2 # 60 -LAST_X = (2 * (TOTAL - 1)) % 32768 -LAST_Y = (2 * (TOTAL - 1) + 1) % 32768 -# Index [MID_IDX][1][0][0][0][0][0] corresponds to element number -# MID_IDX * 64 + 1 * 32 + 0*16 + 0*8 + 0*4 + 0*2 + 0 = MID_IDX*64 + 32 -MID_E = MID_IDX * (2 ** 6) + 32 -MID_X = (2 * MID_E) % 32768 -MID_Y = (2 * MID_E + 1) % 32768 -WRAP_LAST_X = (2 * (TOTAL - 1) + 32768) % 32768 -WRAP_LAST_Y = (2 * (TOTAL - 1) + 1 + 32768) % 32768 - - -def indent(level: int) -> str: - return " " * level - - -def build_tree(shape: list[int], base: int, offset: int) -> tuple[str, int]: - """Recursively build a t27 literal for shape, returning (text, next_base). - - The returned text is a self-contained literal beginning with the full - dimension annotation and ending with the matching closing brace, indented - relative to level 0. - """ - if len(shape) == 1: - n = shape[0] - lines = [f"{indent(1)}[{n}]Pt{{"] - entries = [] - for i in range(n): - e = base + i - x = (2 * e + offset) % 32768 - y = (2 * e + offset + 1) % 32768 - entries.append(f"{indent(2)}Pt{{ .x = {x}, .y = {y} }}") - lines.append(",\n".join(entries)) - lines.append(f"{indent(1)}}}") - return "\n".join(lines), base + n - - n = shape[0] - inner_shape = shape[1:] - dims = "[" + "][".join(str(d) for d in shape) + "]" - lines = [f"{indent(1)}{dims}Pt{{"] - entries = [] - for _ in range(n): - entry_text, base = build_tree(inner_shape, base, offset) - entries.append(entry_text) - lines.append(",\n".join(entries)) - lines.append(f"{indent(1)}}}") - return "\n".join(lines), base - - -def generate() -> None: - shape = [OUTER, 2, 2, 2, 2, 2, 2] - literal, _ = build_tree(shape, 0, 0) - first, rest = literal.split("\n", 1) - indented_lines = [f" {first}"] - for line in rest.splitlines(): - indented_lines.append(f" {line}") - indented_literal = "\n".join(indented_lines) - - dims = "[" + "][".join(str(d) for d in shape) + "]" - make_grid = ( - f"pub fn make_grid(offset : u16) -> {dims}Pt {{\n" - f" return {indented_literal};\n" - "}\n" - ) - - header = f"module w658_bench_module_{OUTER}x2p6_aos_var_call_write\n\n\npub struct Pt {{ x : i16, y : i16 }}\n\n" - - footer = f""" -pub const expected : {dims}Pt = make_grid(0); - -pub var dst : {dims}Pt = make_grid(0); - -test module_var_{OUTER}x2p6_call_write {{ - assert_eq(dst, expected); - assert_eq(dst[0][0][0][0][0][0][0].x, 0); - assert_eq(dst[0][0][0][0][0][0][0].y, 1); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].x, {LAST_X}); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].y, {LAST_Y}); - assert_eq(dst[{MID_IDX}][1][0][0][0][0][0].x, {MID_X}); - assert_eq(dst[{MID_IDX}][1][0][0][0][0][0].y, {MID_Y}); - // Explicit modulo-wrap check: offset 32768 folds back to 0..1 - assert_eq(make_grid(32768)[0][0][0][0][0][0][0].x, 0); - assert_eq(make_grid(32768)[0][0][0][0][0][0][0].y, 1); - assert_eq(make_grid(32768)[{LAST_IDX}][1][1][1][1][1][1].x, {WRAP_LAST_X}); - assert_eq(make_grid(32768)[{LAST_IDX}][1][1][1][1][1][1].y, {WRAP_LAST_Y}); -}} - -bench module_bench_{OUTER}x2p6_call_write {{ - // Site 1: whole-array equality before writes - assert_eq(dst, expected); - - // Site 2: indexed read before writes - assert_eq(dst[0][0][0][0][0][0][0].x, 0); - assert_eq(dst[0][0][0][0][0][0][0].y, 1); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].x, {LAST_X}); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].y, {LAST_Y}); - - // Site 3: indexed signed field writes - dst[0][0][0][0][0][0][0].x = 1234; - dst[0][0][0][0][0][0][0].y = -1234; - dst[{LAST_IDX}][1][1][1][1][1][1].x = -5678; - dst[{LAST_IDX}][1][1][1][1][1][1].y = 5678; - - // Site 4: read back updated fields - assert_eq(dst[0][0][0][0][0][0][0].x, 1234); - assert_eq(dst[0][0][0][0][0][0][0].y, -1234); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].x, -5678); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].y, 5678); - - // Site 5: frame-condition checks on untouched elements - assert_eq(dst[{MID_IDX}][1][0][0][0][0][0].x, {MID_X}); - assert_eq(dst[{MID_IDX}][1][0][0][0][0][0].y, {MID_Y}); - - // Site 6: whole-array inequality after partial writes - // assert_ne is not emitted by the Icarus simulation path, so we verify that - // the changed elements differ from their original expected values. - assert_eq(dst[0][0][0][0][0][0][0].x, 1234); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].x, -5678); -}} -""" - - text = header + make_grid + footer - - if not all(ord(ch) < 128 for ch in text): - raise ValueError("generated spec contains non-ASCII characters") - - with open(DST, "w", encoding="ascii", newline="\n") as f: - f.write(text) - - print(f"Wrote {DST} ({len(text)} bytes, {text.count(chr(10))} lines)") - - -if __name__ == "__main__": - generate() diff --git a/scripts/gen_w659.py b/scripts/gen_w659.py deleted file mode 100644 index c268878cba..0000000000 --- a/scripts/gen_w659.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python3 -"""Generate specs/scratch/w659_bench_module_137x2p6_aos_var_call_write.t27.""" - -import os - -REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DST = os.path.join(REPO, "specs/scratch/w659_bench_module_137x2p6_aos_var_call_write.t27") - -OUTER = 137 -TOTAL = OUTER * 2 ** 6 -LAST_IDX = OUTER - 1 -MID_IDX = OUTER // 2 # 60 -LAST_X = (2 * (TOTAL - 1)) % 32768 -LAST_Y = (2 * (TOTAL - 1) + 1) % 32768 -# Index [MID_IDX][1][0][0][0][0][0] corresponds to element number -# MID_IDX * 64 + 1 * 32 + 0*16 + 0*8 + 0*4 + 0*2 + 0 = MID_IDX*64 + 32 -MID_E = MID_IDX * (2 ** 6) + 32 -MID_X = (2 * MID_E) % 32768 -MID_Y = (2 * MID_E + 1) % 32768 -WRAP_LAST_X = (2 * (TOTAL - 1) + 32768) % 32768 -WRAP_LAST_Y = (2 * (TOTAL - 1) + 1 + 32768) % 32768 - - -def indent(level: int) -> str: - return " " * level - - -def build_tree(shape: list[int], base: int, offset: int) -> tuple[str, int]: - """Recursively build a t27 literal for shape, returning (text, next_base). - - The returned text is a self-contained literal beginning with the full - dimension annotation and ending with the matching closing brace, indented - relative to level 0. - """ - if len(shape) == 1: - n = shape[0] - lines = [f"{indent(1)}[{n}]Pt{{"] - entries = [] - for i in range(n): - e = base + i - x = (2 * e + offset) % 32768 - y = (2 * e + offset + 1) % 32768 - entries.append(f"{indent(2)}Pt{{ .x = {x}, .y = {y} }}") - lines.append(",\n".join(entries)) - lines.append(f"{indent(1)}}}") - return "\n".join(lines), base + n - - n = shape[0] - inner_shape = shape[1:] - dims = "[" + "][".join(str(d) for d in shape) + "]" - lines = [f"{indent(1)}{dims}Pt{{"] - entries = [] - for _ in range(n): - entry_text, base = build_tree(inner_shape, base, offset) - entries.append(entry_text) - lines.append(",\n".join(entries)) - lines.append(f"{indent(1)}}}") - return "\n".join(lines), base - - -def generate() -> None: - shape = [OUTER, 2, 2, 2, 2, 2, 2] - literal, _ = build_tree(shape, 0, 0) - first, rest = literal.split("\n", 1) - indented_lines = [f" {first}"] - for line in rest.splitlines(): - indented_lines.append(f" {line}") - indented_literal = "\n".join(indented_lines) - - dims = "[" + "][".join(str(d) for d in shape) + "]" - make_grid = ( - f"pub fn make_grid(offset : u16) -> {dims}Pt {{\n" - f" return {indented_literal};\n" - "}\n" - ) - - header = f"module w659_bench_module_{OUTER}x2p6_aos_var_call_write\n\n\npub struct Pt {{ x : i16, y : i16 }}\n\n" - - footer = f""" -pub const expected : {dims}Pt = make_grid(0); - -pub var dst : {dims}Pt = make_grid(0); - -test module_var_{OUTER}x2p6_call_write {{ - assert_eq(dst, expected); - assert_eq(dst[0][0][0][0][0][0][0].x, 0); - assert_eq(dst[0][0][0][0][0][0][0].y, 1); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].x, {LAST_X}); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].y, {LAST_Y}); - assert_eq(dst[{MID_IDX}][1][0][0][0][0][0].x, {MID_X}); - assert_eq(dst[{MID_IDX}][1][0][0][0][0][0].y, {MID_Y}); - // Explicit modulo-wrap check: offset 32768 folds back to 0..1 - assert_eq(make_grid(32768)[0][0][0][0][0][0][0].x, 0); - assert_eq(make_grid(32768)[0][0][0][0][0][0][0].y, 1); - assert_eq(make_grid(32768)[{LAST_IDX}][1][1][1][1][1][1].x, {WRAP_LAST_X}); - assert_eq(make_grid(32768)[{LAST_IDX}][1][1][1][1][1][1].y, {WRAP_LAST_Y}); -}} - -bench module_bench_{OUTER}x2p6_call_write {{ - // Site 1: whole-array equality before writes - assert_eq(dst, expected); - - // Site 2: indexed read before writes - assert_eq(dst[0][0][0][0][0][0][0].x, 0); - assert_eq(dst[0][0][0][0][0][0][0].y, 1); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].x, {LAST_X}); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].y, {LAST_Y}); - - // Site 3: indexed signed field writes - dst[0][0][0][0][0][0][0].x = 1234; - dst[0][0][0][0][0][0][0].y = -1234; - dst[{LAST_IDX}][1][1][1][1][1][1].x = -5678; - dst[{LAST_IDX}][1][1][1][1][1][1].y = 5678; - - // Site 4: read back updated fields - assert_eq(dst[0][0][0][0][0][0][0].x, 1234); - assert_eq(dst[0][0][0][0][0][0][0].y, -1234); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].x, -5678); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].y, 5678); - - // Site 5: frame-condition checks on untouched elements - assert_eq(dst[{MID_IDX}][1][0][0][0][0][0].x, {MID_X}); - assert_eq(dst[{MID_IDX}][1][0][0][0][0][0].y, {MID_Y}); - - // Site 6: whole-array inequality after partial writes - // assert_ne is not emitted by the Icarus simulation path, so we verify that - // the changed elements differ from their original expected values. - assert_eq(dst[0][0][0][0][0][0][0].x, 1234); - assert_eq(dst[{LAST_IDX}][1][1][1][1][1][1].x, -5678); -}} -""" - - text = header + make_grid + footer - - if not all(ord(ch) < 128 for ch in text): - raise ValueError("generated spec contains non-ASCII characters") - - with open(DST, "w", encoding="ascii", newline="\n") as f: - f.write(text) - - print(f"Wrote {DST} ({len(text)} bytes, {text.count(chr(10))} lines)") - - -if __name__ == "__main__": - generate() diff --git a/specs/claw/refresh.t27 b/specs/claw/refresh.t27 new file mode 100644 index 0000000000..c69ad2b3f4 --- /dev/null +++ b/specs/claw/refresh.t27 @@ -0,0 +1,1020 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/refresh.t27 -- which open pull requests TRI CLAW brings up to date after a merge, and in +; what order (#10397, lane 4 of epic #10393). +; Owner, 2026-10-18 (translated): move the queue and dispatch into our GitHub App, TRI CLAW, and +; control it with @tri-claw comments; the App runs in GitHub CI. +; -- +; After a merge every open PR whose base moved is behind. A conflict-free one waits for a person or a +; bee to update it, and its CI does not rerun until somebody does. The claw-refresh job in +; .github/workflows/gate-topology.yml mints a token from the update-branch row of github_surface.t27, +; feeds this module the open pull requests, and calls the update-branch endpoint for every line this +; module answers `R`. The job holds no decision; every skip below is decided here. +; -- +; The input, one record per pull request on stdin, eleven TAB fields, written by the job from one +; GraphQL listing and one compare per PR (the fields are copied, never judged): +; number, head sha, createdAt, isDraft, label names joined by commas, isCrossRepository, +; head repository name with owner (empty when the fork is gone), maintainerCanModify, +; mergeable, mergeStateStatus, commits the head is behind its base (`-` when unread) +; The output, one line per fact, two TAB fields: +; R TAB number TAB head sha refresh this one (at most 30, oldest first) +; M TAB text one line for the step summary +; -- +; The decision, first match wins (decide below): +; held the label no-auto-merge is on it: a person or a gate has claimed the PR +; draft drafts are never touched +; fork the head lives in a fork and the author did not allow maintainer edits +; conflict CONFLICTING or DIRTY: left alone, listed once in the summary +; unknown GitHub has not computed mergeability, or the behind count is unread: next push +; current not behind +; refresh behind and conflict-free +; Then at most REFRESH_CAP of the refresh ones are kept, oldest createdAt first (ties: lower number), +; the rest are counted as over the cap and wait for the next merge. +; -- +; Fail closed: a record that is not eleven fields, a number that is not a positive integer, a sha that +; is not 40 lowercase hex digits, a createdAt that is not YYYY-MM-DDTHH:MM:SSZ, a flag that is not +; `true` or `false`, a number seen twice, or a record past MAX_PRS is never refreshed; it is counted +; as unreadable in the summary line. An unread fact never becomes an API call. +; The honest limit: update-branch merges the base into the PR head, so the head moves and its CI +; reruns; it does not rebase, and it cannot resolve a conflict. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawRefresh; + +use claw::sarif; +use claw::text; + +/// Records one run reads; more are counted as unreadable, never silently dropped. +pub const MAX_PRS : usize = 512; +/// FR-002: the most pull requests one run refreshes, so one merge cannot flood the Actions queue. +pub const REFRESH_CAP : u32 = 30; +/// Fields of one record. +pub const NFIELDS : usize = 11; + +/// Decisions. D_BAD is an unreadable record. +pub const D_NONE : u8 = 0; +pub const D_REFRESH : u8 = 1; +pub const D_CONFLICT : u8 = 2; +pub const D_HELD : u8 = 3; +pub const D_FORK : u8 = 4; +pub const D_DRAFT : u8 = 5; +pub const D_CURRENT : u8 = 6; +pub const D_UNKNOWN : u8 = 7; +pub const D_CAPPED : u8 = 8; +pub const D_BAD : u8 = 9; +pub const D_COUNT : usize = 10; + +/// What GitHub says about mergeability, folded: M_OK may be updated, M_CONFLICT may not, M_UNKNOWN is +/// not computed yet (a draft, or a PR whose base moved a moment ago). +pub const M_OK : u8 = 0; +pub const M_CONFLICT : u8 = 1; +pub const M_UNKNOWN : u8 = 2; + +/// A flag as the job copies it: `true`, `false`, or anything else (refused). +pub const T_FALSE : u8 = 0; +pub const T_TRUE : u8 = 1; +pub const T_BAD : u8 = 2; + +/// The label that holds a pull request out of every automatic step (issue-gate.yml carries the same word). +pub const HELD_LABEL : str = "no-auto-merge"; +pub const HELD_LABEL_LEN : usize = 13; + +/// Summary pieces, each with its byte length (the tests pin the lengths). +pub const SUM_HEAD : str = "M\tclaw-refresh: "; +pub const SUM_HEAD_LEN : usize = 16; +pub const SUM_TEXTS : [8]str = [ + " to update, ", " in conflict, ", " held or draft, ", " fork without maintainer edits, ", + " already up to date, ", " not decidable yet, ", " over the cap, ", " unreadable\n", +]; +pub const SUM_LENS : [8]usize = [12, 14, 16, 32, 21, 20, 15, 12]; +pub const CONFLICT_HEAD : str = "M\t- #"; +pub const CONFLICT_HEAD_LEN : usize = 5; +pub const CONFLICT_TAIL : str = " conflicts with its base branch and was left alone\n"; +pub const CONFLICT_TAIL_LEN : usize = 51; +pub const BLANK_LINE : str = "M\t\n"; +pub const BLANK_LINE_LEN : usize = 3; + +/// The pure decision. fork_blocked is "the head is in a fork and maintainer edits are off"; mc is +/// M_*; bh is 1 behind, 0 current, -1 unread. +pub fn decide(held : bool, draft : bool, fork_blocked : bool, mc : u8, bh : i32) -> u8 { + if (held) { return D_HELD; } + if (draft) { return D_DRAFT; } + if (fork_blocked) { return D_FORK; } + if (mc == M_CONFLICT) { return D_CONFLICT; } + if (mc == M_UNKNOWN) { return D_UNKNOWN; } + if (bh < 0) { return D_UNKNOWN; } + if (bh == 0) { return D_CURRENT; } + return D_REFRESH; +} + +/// Whether raw[s..e) is a comma list with the held label as one whole entry. +pub fn rf_label_held(raw : []const u8, s : usize, e : usize) -> bool { + var p : usize = s; + while (p <= e) { + var q : usize = p; + while (q < e and raw[q] != ',') { q += 1; } + if (field_eq(raw, p, q, HELD_LABEL, HELD_LABEL_LEN)) { return true; } + p = q + 1; + } + return false; +} + +/// `true`, `false` or neither. +pub fn rf_flag(raw : []const u8, s : usize, e : usize) -> u8 { + if (field_eq(raw, s, e, "true", 4)) { return T_TRUE; } + if (field_eq(raw, s, e, "false", 5)) { return T_FALSE; } + return T_BAD; +} + +/// Mergeability folded to M_*: CONFLICTING or DIRTY is a conflict, MERGEABLE is fine, everything else +/// (UNKNOWN, a word GitHub adds later) is not decidable. +pub fn rf_merge_class(raw : []const u8, ms : usize, me : usize, ss : usize, se : usize) -> u8 { + if (field_eq(raw, ms, me, "CONFLICTING", 11)) { return M_CONFLICT; } + if (field_eq(raw, ss, se, "DIRTY", 5)) { return M_CONFLICT; } + if (field_eq(raw, ms, me, "MERGEABLE", 9)) { return M_OK; } + return M_UNKNOWN; +} + +/// 1 when the head is behind its base, 0 when it is not, -1 when neither the state word nor the +/// count says: BEHIND wins, then a positive count, then a zero count. +pub fn rf_behind(raw : []const u8, bs : usize, be : usize, ss : usize, se : usize) -> i32 { + if (field_eq(raw, ss, se, "BEHIND", 6)) { return 1; } + const n : i32 = parse_num(raw, bs, be); + if (n > 0) { return 1; } + if (n == 0) { return 0; } + return -1; +} + +fn rf_hex(b : u8) -> bool { + if (b >= '0' and b <= '9') { return true; } + return b >= 'a' and b <= 'f'; +} + +fn rf_digit(b : u8) -> bool { + return b >= '0' and b <= '9'; +} + +/// 40 lowercase hex digits. +pub fn rf_sha_ok(raw : []const u8, s : usize, e : usize) -> bool { + if (e - s != 40) { return false; } + var bad : usize = 0; + var i : usize = s; + while (i < e) { + if (rf_hex(raw[i]) == false) { bad += 1; } + i += 1; + } + return bad == 0; +} + +/// YYYY-MM-DDTHH:MM:SSZ, 20 bytes: the shape that sorts the same as time does. +pub fn rf_created_ok(raw : []const u8, s : usize, e : usize) -> bool { + if (e - s != 20) { return false; } + var i : usize = 0; + while (i < 20) { + const b : u8 = raw[s + i]; + if (i == 4 or i == 7) { + if (b != '-') { return false; } + } else if (i == 10) { + if (b != 'T') { return false; } + } else if (i == 13 or i == 16) { + if (b != ':') { return false; } + } else if (i == 19) { + if (b != 'Z') { return false; } + } else if (rf_digit(b) == false) { + return false; + } + i += 1; + } + return true; +} + +/// Whether the PR created at raw[ca..ca+20) with number na is older than the one at cb with number +/// nb: earlier time first, the lower number when the seconds are equal. +pub fn rf_older(raw : []const u8, ca : usize, na : u32, cb : usize, nb : u32) -> bool { + var k : usize = 0; + while (k < 20) { + const x : u8 = raw[ca + k]; + const y : u8 = raw[cb + k]; + if (x < y) { return true; } + if (x > y) { return false; } + k += 1; + } + return na < nb; +} + +/// Split raw[ls..le) into exactly NFIELDS tab fields: f[2k] is the start and f[2k+1] the end of +/// field k. False for fewer or more. +fn rf_split(raw : []const u8, ls : usize, le : usize, f : []usize) -> bool { + var k : usize = 0; + var s : usize = ls; + while (k < NFIELDS) { + const e : usize = field_end(raw, le, s); + f[2 * k] = s; + f[2 * k + 1] = e; + k += 1; + if (k < NFIELDS) { + if (e >= le) { return false; } + s = e + 1; + } else { + return e == le; + } + } + return false; +} + +/// The decision for one split record; D_BAD when any fact it needs cannot be read. +fn rf_classify(raw : []const u8, f : []const usize) -> u8 { + if (rf_sha_ok(raw, f[2], f[3]) == false) { return D_BAD; } + if (rf_created_ok(raw, f[4], f[5]) == false) { return D_BAD; } + const draft : u8 = rf_flag(raw, f[6], f[7]); + const cross : u8 = rf_flag(raw, f[10], f[11]); + const modify : u8 = rf_flag(raw, f[14], f[15]); + if (draft == T_BAD or cross == T_BAD or modify == T_BAD) { return D_BAD; } + const held : bool = rf_label_held(raw, f[8], f[9]); + const gone : bool = f[13] == f[12]; + const fork_blocked : bool = (cross == T_TRUE or gone) and modify != T_TRUE; + const mc : u8 = rf_merge_class(raw, f[16], f[17], f[18], f[19]); + const bh : i32 = rf_behind(raw, f[20], f[21], f[18], f[19]); + return decide(held, draft == T_TRUE, fork_blocked, mc, bh); +} + +/// Index of the decision in the summary texts, for the counts that share one text. +fn rf_put_sha(dst : []u8, cap : usize, w : usize, raw : []const u8, s : usize) -> usize { + var x : usize = w; + var i : usize = 0; + while (i < 40) { + x = put_byte(dst, cap, x, raw[s + i]); + i += 1; + } + return x; +} + +/// The answer for the records in raw[0..n), into out: the R lines oldest first, then the summary. +/// Returns the bytes it needs; when that exceeds cap the buffer holds a clipped answer that must not +/// be acted on. +pub fn refresh_plan(raw : []const u8, n : usize, out : []u8, cap : usize) -> usize { + var dec : [512]u8 = undefined; + var num : [512]u32 = undefined; + var cs : [512]usize = undefined; + var ss : [512]usize = undefined; + var rk : [512]u32 = undefined; + var f : [22]usize = undefined; + var st : [10]u32 = undefined; + var cnt : usize = 0; + var extra : u32 = 0; + var ls : usize = 0; + while (ls < n) { + const le : usize = line_end(raw, n, ls); + if (le > ls) { + if (cnt >= MAX_PRS) { + extra += 1; + } else { + var d : u8 = D_BAD; + var v : u32 = 0; + cs[cnt] = 0; + ss[cnt] = 0; + if (rf_split(raw, ls, le, &f)) { + const pn : i32 = parse_num(raw, f[0], f[1]); + if (pn > 0) { + v = pn as u32; + d = rf_classify(raw, &f); + cs[cnt] = f[4]; + ss[cnt] = f[2]; + } + } + var j : usize = 0; + while (j < cnt) { + if (v != 0 and num[j] == v) { d = D_BAD; } + j += 1; + } + num[cnt] = v; + dec[cnt] = d; + cnt += 1; + } + } + var nx : usize = le; + while (nx < n and raw[nx] != '\n') { nx += 1; } + ls = nx + 1; + } + var i : usize = 0; + while (i < cnt) { + rk[i] = 0; + if (dec[i] == D_REFRESH) { + var j : usize = 0; + while (j < cnt) { + if (j != i and dec[j] == D_REFRESH) { + if (rf_older(raw, cs[j], num[j], cs[i], num[i])) { rk[i] += 1; } + } + j += 1; + } + } + i += 1; + } + i = 0; + while (i < cnt) { + if (dec[i] == D_REFRESH and rk[i] >= REFRESH_CAP) { dec[i] = D_CAPPED; } + i += 1; + } + var c : usize = 0; + while (c < D_COUNT) { + st[c] = 0; + c += 1; + } + i = 0; + while (i < cnt) { + st[dec[i] as usize] += 1; + i += 1; + } + st[D_BAD as usize] += extra; + var w : usize = 0; + var r : u32 = 0; + while (r < REFRESH_CAP) { + i = 0; + while (i < cnt) { + if (dec[i] == D_REFRESH and rk[i] == r) { + w = put_str(out, cap, w, "R\t", 2); + w = put_count(out, cap, w, num[i]); + w = put_byte(out, cap, w, '\t'); + w = rf_put_sha(out, cap, w, raw, ss[i]); + w = put_byte(out, cap, w, '\n'); + } + i += 1; + } + r += 1; + } + w = put_str(out, cap, w, SUM_HEAD, SUM_HEAD_LEN); + w = put_count(out, cap, w, st[D_REFRESH as usize]); + w = put_str(out, cap, w, SUM_TEXTS[0], SUM_LENS[0]); + w = put_count(out, cap, w, st[D_CONFLICT as usize]); + w = put_str(out, cap, w, SUM_TEXTS[1], SUM_LENS[1]); + w = put_count(out, cap, w, st[D_HELD as usize] + st[D_DRAFT as usize]); + w = put_str(out, cap, w, SUM_TEXTS[2], SUM_LENS[2]); + w = put_count(out, cap, w, st[D_FORK as usize]); + w = put_str(out, cap, w, SUM_TEXTS[3], SUM_LENS[3]); + w = put_count(out, cap, w, st[D_CURRENT as usize]); + w = put_str(out, cap, w, SUM_TEXTS[4], SUM_LENS[4]); + w = put_count(out, cap, w, st[D_UNKNOWN as usize]); + w = put_str(out, cap, w, SUM_TEXTS[5], SUM_LENS[5]); + w = put_count(out, cap, w, st[D_CAPPED as usize]); + w = put_str(out, cap, w, SUM_TEXTS[6], SUM_LENS[6]); + w = put_count(out, cap, w, st[D_BAD as usize]); + w = put_str(out, cap, w, SUM_TEXTS[7], SUM_LENS[7]); + if (st[D_CONFLICT as usize] > 0) { + w = put_str(out, cap, w, BLANK_LINE, BLANK_LINE_LEN); + i = 0; + while (i < cnt) { + if (dec[i] == D_CONFLICT) { + w = put_str(out, cap, w, CONFLICT_HEAD, CONFLICT_HEAD_LEN); + w = put_count(out, cap, w, num[i]); + w = put_str(out, cap, w, CONFLICT_TAIL, CONFLICT_TAIL_LEN); + } + i += 1; + } + } + return w; +} + +/// Whether the answer of refresh_plan fits the buffer it was written to. +pub fn refresh_fits(needed : usize, out_cap : usize) -> bool { + return needed <= out_cap; +} + +// ---- test helpers: build one record from small codes, so a test states the case and not the bytes ---- + +fn t_put2(dst : []u8, cap : usize, w : usize, v : u32) -> usize { + var x : usize = put_byte(dst, cap, w, digit_byte(v / 10)); + x = put_byte(dst, cap, x, digit_byte(v % 10)); + return x; +} + +/// One record. ts is seconds into 2026-10-18 (under a day); labels: 0 none, 1 no-auto-merge, +/// 2 bug,no-auto-merge,x, 3 no-auto-merge-2, 4 x-no-auto-merge, 5 bug,x; mg: 0 MERGEABLE, +/// 1 CONFLICTING, 2 UNKNOWN; sc: 0 BLOCKED, 1 DIRTY, 2 BEHIND, 3 UNKNOWN; behind: commits behind, +/// 9999 for unread. The sha is 40 hex digits whose first is the last digit of num. +fn t_rec(dst : []u8, cap : usize, w : usize, num : u32, ts : u32, draft : bool, labels : u32, cross : bool, modify : bool, mg : u32, sc : u32, behind : u32) -> usize { + var x : usize = put_count(dst, cap, w, num); + x = put_byte(dst, cap, x, '\t'); + x = put_byte(dst, cap, x, digit_byte(num % 10)); + var k : usize = 1; + while (k < 40) { + x = put_byte(dst, cap, x, 'a'); + k += 1; + } + x = put_str(dst, cap, x, "\t2026-10-18T", 12); + x = t_put2(dst, cap, x, ts / 3600); + x = put_byte(dst, cap, x, ':'); + x = t_put2(dst, cap, x, (ts % 3600) / 60); + x = put_byte(dst, cap, x, ':'); + x = t_put2(dst, cap, x, ts % 60); + x = put_str(dst, cap, x, "Z\t", 2); + if (draft) { x = put_str(dst, cap, x, "true", 4); } else { x = put_str(dst, cap, x, "false", 5); } + x = put_byte(dst, cap, x, '\t'); + if (labels == 1) { x = put_str(dst, cap, x, "no-auto-merge", 13); } + if (labels == 2) { x = put_str(dst, cap, x, "bug,no-auto-merge,x", 19); } + if (labels == 3) { x = put_str(dst, cap, x, "no-auto-merge-2", 15); } + if (labels == 4) { x = put_str(dst, cap, x, "x-no-auto-merge", 15); } + if (labels == 5) { x = put_str(dst, cap, x, "bug,x", 5); } + x = put_byte(dst, cap, x, '\t'); + if (cross) { x = put_str(dst, cap, x, "true\tsomeone/t27\t", 17); } else { x = put_str(dst, cap, x, "false\tgHashTag/t27\t", 19); } + if (modify) { x = put_str(dst, cap, x, "true\t", 5); } else { x = put_str(dst, cap, x, "false\t", 6); } + if (mg == 0) { x = put_str(dst, cap, x, "MERGEABLE", 9); } + if (mg == 1) { x = put_str(dst, cap, x, "CONFLICTING", 11); } + if (mg == 2) { x = put_str(dst, cap, x, "UNKNOWN", 7); } + x = put_byte(dst, cap, x, '\t'); + if (sc == 0) { x = put_str(dst, cap, x, "BLOCKED", 7); } + if (sc == 1) { x = put_str(dst, cap, x, "DIRTY", 5); } + if (sc == 2) { x = put_str(dst, cap, x, "BEHIND", 6); } + if (sc == 3) { x = put_str(dst, cap, x, "UNKNOWN", 7); } + x = put_byte(dst, cap, x, '\t'); + if (behind == 9999) { + x = put_byte(dst, cap, x, '-'); + } else { + x = put_count(dst, cap, x, behind); + } + return put_byte(dst, cap, x, '\n'); +} + +/// Lines of out[0..n) that start with byte b. +fn t_lines(out : []const u8, n : usize, b : u8) -> u32 { + var c : u32 = 0; + var i : usize = 0; + while (i < n) { + if (out[i] == b and (i == 0 or out[i - 1] == '\n')) { c += 1; } + i += 1; + } + return c; +} + +// ---- tests: the pure decision ---- + +test a_behind_conflict_free_pull_request_is_refreshed { + assert decide(false, false, false, M_OK, 3) == D_REFRESH; + assert decide(false, false, false, M_OK, 1) == D_REFRESH; +} + +test a_pull_request_that_is_not_behind_is_left_alone { + assert decide(false, false, false, M_OK, 0) == D_CURRENT; +} + +test a_conflicting_pull_request_is_never_refreshed { + assert decide(false, false, false, M_CONFLICT, 5) == D_CONFLICT; + assert decide(false, false, false, M_CONFLICT, 0) == D_CONFLICT; +} + +test a_held_pull_request_is_never_touched_whatever_else_is_true { + assert decide(true, false, false, M_OK, 5) == D_HELD; + assert decide(true, false, false, M_CONFLICT, 5) == D_HELD; + assert decide(true, true, true, M_UNKNOWN, 0) == D_HELD; +} + +test a_draft_is_never_touched { + assert decide(false, true, false, M_OK, 5) == D_DRAFT; + assert decide(false, true, false, M_CONFLICT, 5) == D_DRAFT; +} + +test a_fork_without_maintainer_edits_is_skipped_not_failed { + assert decide(false, false, true, M_OK, 5) == D_FORK; + assert decide(false, false, true, M_CONFLICT, 5) == D_FORK; +} + +test a_fork_with_maintainer_edits_is_refreshed_like_any_other { + assert decide(false, false, false, M_OK, 5) == D_REFRESH; +} + +test unread_facts_never_become_a_refresh { + assert decide(false, false, false, M_UNKNOWN, 5) == D_UNKNOWN; + assert decide(false, false, false, M_OK, -1) == D_UNKNOWN; +} + +// ---- tests: reading the fields ---- + +test the_held_label_matches_only_as_a_whole_entry { + var b : [64]u8 = undefined; + var n : usize = put_str(&b, 64, 0, "no-auto-merge", 13); + assert rf_label_held(&b, 0, n) == true; + n = put_str(&b, 64, 0, "bug,no-auto-merge,x", 19); + assert rf_label_held(&b, 0, n) == true; + n = put_str(&b, 64, 0, "bug,no-auto-merge", 17); + assert rf_label_held(&b, 0, n) == true; + n = put_str(&b, 64, 0, "no-auto-merge-2", 15); + assert rf_label_held(&b, 0, n) == false; + n = put_str(&b, 64, 0, "x-no-auto-merge", 15); + assert rf_label_held(&b, 0, n) == false; + n = put_str(&b, 64, 0, "no-auto-merg", 12); + assert rf_label_held(&b, 0, n) == false; + n = put_str(&b, 64, 0, "bug,x", 5); + assert rf_label_held(&b, 0, n) == false; + assert rf_label_held(&b, 0, 0) == false; +} + +test a_flag_is_true_false_or_refused { + var b : [16]u8 = undefined; + var n : usize = put_str(&b, 16, 0, "true", 4); + assert rf_flag(&b, 0, n) == T_TRUE; + n = put_str(&b, 16, 0, "false", 5); + assert rf_flag(&b, 0, n) == T_FALSE; + n = put_str(&b, 16, 0, "True", 4); + assert rf_flag(&b, 0, n) == T_BAD; + n = put_str(&b, 16, 0, "", 0); + assert rf_flag(&b, 0, n) == T_BAD; + n = put_str(&b, 16, 0, "1", 1); + assert rf_flag(&b, 0, n) == T_BAD; +} + +test mergeability_folds_conflict_and_unknown_apart { + var b : [64]u8 = undefined; + var m : usize = put_str(&b, 64, 0, "MERGEABLE", 9); + var s : usize = put_str(&b, 64, m, "BLOCKED", 7); + assert rf_merge_class(&b, 0, m, m, s) == M_OK; + s = put_str(&b, 64, m, "DIRTY", 5); + assert rf_merge_class(&b, 0, m, m, s) == M_CONFLICT; + m = put_str(&b, 64, 0, "CONFLICTING", 11); + s = put_str(&b, 64, m, "BLOCKED", 7); + assert rf_merge_class(&b, 0, m, m, s) == M_CONFLICT; + m = put_str(&b, 64, 0, "UNKNOWN", 7); + s = put_str(&b, 64, m, "UNKNOWN", 7); + assert rf_merge_class(&b, 0, m, m, s) == M_UNKNOWN; + m = put_str(&b, 64, 0, "WHATEVER", 8); + s = put_str(&b, 64, m, "CLEAN", 5); + assert rf_merge_class(&b, 0, m, m, s) == M_UNKNOWN; +} + +test behind_reads_the_state_word_then_the_count { + var b : [64]u8 = undefined; + var c : usize = put_str(&b, 64, 0, "4", 1); + var s : usize = put_str(&b, 64, c, "BLOCKED", 7); + assert rf_behind(&b, 0, c, c, s) == 1; + c = put_str(&b, 64, 0, "0", 1); + s = put_str(&b, 64, c, "BLOCKED", 7); + assert rf_behind(&b, 0, c, c, s) == 0; + s = put_str(&b, 64, c, "BEHIND", 6); + assert rf_behind(&b, 0, c, c, s) == 1; + c = put_str(&b, 64, 0, "-", 1); + s = put_str(&b, 64, c, "BLOCKED", 7); + assert rf_behind(&b, 0, c, c, s) == -1; + s = put_str(&b, 64, c, "BEHIND", 6); + assert rf_behind(&b, 0, c, c, s) == 1; +} + +test a_sha_is_forty_lowercase_hex_digits { + var b : [64]u8 = undefined; + var n : usize = 0; + while (n < 40) { + b[n] = 'a'; + n += 1; + } + assert rf_sha_ok(&b, 0, 40) == true; + assert rf_sha_ok(&b, 0, 39) == false; + b[7] = 'G'; + assert rf_sha_ok(&b, 0, 40) == false; + b[7] = 'A'; + assert rf_sha_ok(&b, 0, 40) == false; + b[7] = '9'; + assert rf_sha_ok(&b, 0, 40) == true; + b[7] = 'f'; + assert rf_sha_ok(&b, 0, 40) == true; + b[7] = 'g'; + assert rf_sha_ok(&b, 0, 40) == false; +} + +test a_created_stamp_has_exactly_the_sortable_shape { + var b : [32]u8 = undefined; + var n : usize = put_str(&b, 32, 0, "2026-10-18T09:05:01Z", 20); + assert rf_created_ok(&b, 0, n) == true; + n = put_str(&b, 32, 0, "2026-10-18 09:05:01Z", 20); + assert rf_created_ok(&b, 0, n) == false; + n = put_str(&b, 32, 0, "2026-10-18T09:05:01+0", 20); + assert rf_created_ok(&b, 0, n) == false; + n = put_str(&b, 32, 0, "2026-10-18T09:05:01", 19); + assert rf_created_ok(&b, 0, n) == false; + n = put_str(&b, 32, 0, "2026-1x-18T09:05:01Z", 20); + assert rf_created_ok(&b, 0, n) == false; +} + +test older_goes_by_time_then_by_number { + var b : [64]u8 = undefined; + const a : usize = put_str(&b, 64, 0, "2026-10-18T09:05:01Z", 20); + const c : usize = put_str(&b, 64, a, "2026-10-18T09:05:02Z", 20); + assert rf_older(&b, 0, 900, a, 100) == true; + assert rf_older(&b, a, 100, 0, 900) == false; + assert rf_older(&b, 0, 5, 0, 6) == true; + assert rf_older(&b, 0, 6, 0, 5) == false; + assert rf_older(&b, 0, 5, 0, 5) == false; + assert c == 40; +} + +// ---- tests: whole records through refresh_plan ---- + +test a_behind_clean_pull_request_gets_an_R_line_with_its_sha { + var r : [256]u8 = undefined; + const n : usize = t_rec(&r, 256, 0, 4217, 600, false, 0, false, false, 0, 0, 3); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert refresh_fits(d, 4096) == true; + assert out[0] == 'R'; + assert out[1] == '\t'; + assert buf_has(&out, d, "R\t4217\t7aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", 48) == true; + assert t_lines(&out, d, 'R') == 1; + assert buf_has(&out, d, "M\tclaw-refresh: 1 to update, 0 in conflict, 0 held or draft, 0 fork without maintainer edits, 0 already up to date, 0 not decidable yet, 0 over the cap, 0 unreadable\n", 166) == true; +} + +test a_conflicting_pull_request_is_listed_once_and_not_refreshed { + var r : [512]u8 = undefined; + var n : usize = t_rec(&r, 512, 0, 300, 100, false, 0, false, false, 1, 1, 7); + n = t_rec(&r, 512, n, 301, 200, false, 0, false, false, 0, 0, 2); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 1; + assert buf_has(&out, d, "R\t301\t", 6) == true; + assert buf_has(&out, d, "R\t300\t", 6) == false; + assert buf_has(&out, d, "M\t- #300 conflicts with its base branch and was left alone\n", 59) == true; + assert buf_has(&out, d, "M\t- #301", 8) == false; + assert buf_has(&out, d, " 1 in conflict, ", 16) == true; +} + +test a_dirty_state_word_alone_is_a_conflict { + var r : [256]u8 = undefined; + const n : usize = t_rec(&r, 256, 0, 310, 100, false, 0, false, false, 0, 1, 4); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 0; + assert buf_has(&out, d, "M\t- #310 ", 9) == true; +} + +test a_draft_and_a_held_pull_request_are_not_touched { + var r : [1024]u8 = undefined; + var n : usize = t_rec(&r, 1024, 0, 400, 100, true, 0, false, false, 0, 0, 9); + n = t_rec(&r, 1024, n, 401, 110, false, 1, false, false, 0, 0, 9); + n = t_rec(&r, 1024, n, 402, 120, false, 2, false, false, 0, 0, 9); + n = t_rec(&r, 1024, n, 403, 130, false, 3, false, false, 0, 0, 9); + n = t_rec(&r, 1024, n, 404, 140, false, 4, false, false, 0, 0, 9); + n = t_rec(&r, 1024, n, 405, 150, false, 5, false, false, 0, 0, 9); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert buf_has(&out, d, "R\t400\t", 6) == false; + assert buf_has(&out, d, "R\t401\t", 6) == false; + assert buf_has(&out, d, "R\t402\t", 6) == false; + assert buf_has(&out, d, "R\t403\t", 6) == true; + assert buf_has(&out, d, "R\t404\t", 6) == true; + assert buf_has(&out, d, "R\t405\t", 6) == true; + assert t_lines(&out, d, 'R') == 3; + assert buf_has(&out, d, " 3 held or draft, ", 18) == true; +} + +test a_fork_without_maintainer_edits_is_skipped_and_one_with_edits_is_refreshed { + var r : [1024]u8 = undefined; + var n : usize = t_rec(&r, 1024, 0, 500, 100, false, 0, true, false, 0, 0, 9); + n = t_rec(&r, 1024, n, 501, 110, false, 0, true, true, 0, 0, 9); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert buf_has(&out, d, "R\t500\t", 6) == false; + assert buf_has(&out, d, "R\t501\t", 6) == true; + assert t_lines(&out, d, 'R') == 1; + assert buf_has(&out, d, " 1 fork without maintainer edits, ", 34) == true; +} + +test a_deleted_head_repository_counts_as_a_fork_that_cannot_be_edited { + var r : [256]u8 = undefined; + var n : usize = put_str(&r, 256, 0, "510\t", 4); + var k : usize = 0; + while (k < 40) { + n = put_byte(&r, 256, n, 'b'); + k += 1; + } + n = put_str(&r, 256, n, "\t2026-10-18T00:01:00Z\tfalse\t\tfalse\t\tfalse\tMERGEABLE\tBLOCKED\t5\n", 62); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 0; + assert buf_has(&out, d, " 1 fork without maintainer edits, ", 34) == true; +} + +test a_pull_request_that_is_current_or_not_computed_is_counted_and_left_alone { + var r : [1024]u8 = undefined; + var n : usize = t_rec(&r, 1024, 0, 600, 100, false, 0, false, false, 0, 0, 0); + n = t_rec(&r, 1024, n, 601, 110, false, 0, false, false, 2, 3, 5); + n = t_rec(&r, 1024, n, 602, 120, false, 0, false, false, 0, 0, 9999); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 0; + assert buf_has(&out, d, " 1 already up to date, ", 23) == true; + assert buf_has(&out, d, " 2 not decidable yet, ", 22) == true; +} + +test a_record_that_cannot_be_read_is_counted_and_never_refreshed { + var r : [2048]u8 = undefined; + var n : usize = put_str(&r, 2048, 0, "700\tnotasha\t2026-10-18T00:01:00Z\tfalse\t\tfalse\tx/y\tfalse\tMERGEABLE\tBLOCKED\t5\n", 76); + n = put_str(&r, 2048, n, "701\tshort record\n", 17); + n = put_str(&r, 2048, n, "abc\tdef\n", 8); + n = put_str(&r, 2048, n, "0\t", 2); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 0; + assert buf_has(&out, d, " 4 unreadable\n", 14) == true; +} + +test a_flag_that_is_not_true_or_false_makes_the_record_unreadable { + var r : [512]u8 = undefined; + var n : usize = t_rec(&r, 512, 0, 710, 100, false, 0, false, false, 0, 0, 5); + var k : usize = 0; + while (k < n) { + if (r[k] == 'f' and r[k + 1] == 'a' and r[k + 2] == 'l' and r[k + 3] == 's' and r[k + 4] == 'e') { + r[k] = 'F'; + k = n; + } + k += 1; + } + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 0; + assert buf_has(&out, d, " 1 unreadable\n", 14) == true; +} + +test a_number_seen_twice_is_refused_the_second_time { + var r : [1024]u8 = undefined; + var n : usize = t_rec(&r, 1024, 0, 800, 100, false, 0, false, false, 0, 0, 5); + n = t_rec(&r, 1024, n, 800, 200, false, 0, false, false, 0, 0, 5); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 1; + assert buf_has(&out, d, " 1 unreadable\n", 14) == true; +} + +test the_oldest_pull_request_comes_first_whatever_the_input_order { + var r : [1024]u8 = undefined; + var n : usize = t_rec(&r, 1024, 0, 903, 3000, false, 0, false, false, 0, 0, 1); + n = t_rec(&r, 1024, n, 901, 1000, false, 0, false, false, 0, 0, 1); + n = t_rec(&r, 1024, n, 902, 2000, false, 0, false, false, 0, 0, 1); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 3; + assert out[0] == 'R'; + assert buf_has(&out, d, "R\t901\t", 6) == true; + assert out[2] == '9'; + assert out[3] == '0'; + assert out[4] == '1'; + assert out[47] == 'R'; + assert out[49] == '9'; + assert out[50] == '0'; + assert out[51] == '2'; + assert out[94] == 'R'; + assert out[98] == '3'; +} + +test equal_timestamps_are_ordered_by_number { + var r : [1024]u8 = undefined; + var n : usize = t_rec(&r, 1024, 0, 952, 500, false, 0, false, false, 0, 0, 1); + n = t_rec(&r, 1024, n, 951, 500, false, 0, false, false, 0, 0, 1); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert out[4] == '1'; + assert out[5] == '\t'; +} + +test at_most_thirty_are_refreshed_and_they_are_the_oldest { + var r : [8192]u8 = undefined; + var n : usize = 0; + var i : u32 = 35; + while (i > 0) { + n = t_rec(&r, 8192, n, 100 + i, i * 60, false, 0, false, false, 0, 0, 2); + i -= 1; + } + var out : [16384]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 16384); + assert refresh_fits(d, 16384) == true; + assert t_lines(&out, d, 'R') == 30; + assert buf_has(&out, d, "R\t101\t", 6) == true; + assert buf_has(&out, d, "R\t130\t", 6) == true; + assert buf_has(&out, d, "R\t131\t", 6) == false; + assert buf_has(&out, d, "R\t135\t", 6) == false; + assert buf_has(&out, d, " 30 to update, ", 15) == true; + assert buf_has(&out, d, " 5 over the cap, ", 17) == true; +} + +test thirty_exactly_is_not_capped { + var r : [8192]u8 = undefined; + var n : usize = 0; + var i : u32 = 1; + while (i <= 30) { + n = t_rec(&r, 8192, n, 200 + i, i * 60, false, 0, false, false, 0, 0, 2); + i += 1; + } + var out : [16384]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 16384); + assert t_lines(&out, d, 'R') == 30; + assert buf_has(&out, d, " 0 over the cap, ", 17) == true; +} + +test held_and_conflicting_ones_do_not_use_up_the_cap { + var r : [16384]u8 = undefined; + var n : usize = 0; + var i : u32 = 1; + while (i <= 10) { + n = t_rec(&r, 16384, n, 300 + i, i * 60, false, 1, false, false, 0, 0, 2); + i += 1; + } + i = 11; + while (i <= 20) { + n = t_rec(&r, 16384, n, 300 + i, i * 60, false, 0, false, false, 1, 1, 2); + i += 1; + } + i = 21; + while (i <= 50) { + n = t_rec(&r, 16384, n, 300 + i, i * 60, false, 0, false, false, 0, 0, 2); + i += 1; + } + var out : [16384]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 16384); + assert t_lines(&out, d, 'R') == 30; + assert buf_has(&out, d, "R\t321\t", 6) == true; + assert buf_has(&out, d, "R\t350\t", 6) == true; + assert t_lines(&out, d, 'M') == 12; +} + +test an_empty_list_is_a_valid_empty_answer { + var r : [4]u8 = undefined; + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, 0, &out, 4096); + assert t_lines(&out, d, 'R') == 0; + assert buf_has(&out, d, "M\tclaw-refresh: 0 to update, 0 in conflict, 0 held or draft, 0 fork without maintainer edits, 0 already up to date, 0 not decidable yet, 0 over the cap, 0 unreadable\n", 166) == true; + assert t_lines(&out, d, 'M') == 1; +} + +test a_carriage_return_does_not_hide_a_record { + var r : [512]u8 = undefined; + var n : usize = t_rec(&r, 512, 0, 970, 100, false, 0, false, false, 0, 0, 2); + r[n - 1] = 13; + n = put_byte(&r, 512, n, '\n'); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 1; +} + +test a_buffer_too_small_is_reported_not_trusted { + var r : [256]u8 = undefined; + const n : usize = t_rec(&r, 256, 0, 980, 100, false, 0, false, false, 0, 0, 2); + var out : [32]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 32); + assert refresh_fits(d, 32) == false; + assert refresh_fits(d, d) == true; +} + +test the_summary_texts_are_pinned_by_their_lengths { + var b : [64]u8 = undefined; + var k : usize = 0; + while (k < 8) { + const w : usize = put_str(&b, 64, 0, SUM_TEXTS[k], SUM_LENS[k]); + assert w == SUM_LENS[k]; + assert b[0] == ' '; + k += 1; + } + var w : usize = put_str(&b, 64, 0, SUM_TEXTS[7], SUM_LENS[7]); + assert b[w - 1] == '\n'; + assert b[w - 2] == 'e'; + w = put_str(&b, 64, 0, CONFLICT_TAIL, CONFLICT_TAIL_LEN); + assert b[w - 1] == '\n'; + assert b[w - 2] == 'e'; + w = put_str(&b, 64, 0, SUM_HEAD, SUM_HEAD_LEN); + assert b[w - 1] == ' '; + assert b[w - 2] == ':'; + w = put_str(&b, 64, 0, CONFLICT_HEAD, CONFLICT_HEAD_LEN); + assert b[w - 1] == '#'; + w = put_str(&b, 64, 0, BLANK_LINE, BLANK_LINE_LEN); + assert b[w - 1] == '\n'; + assert b[w - 2] == '\t'; +} + +test one_record_splits_into_eleven_fields_and_classifies_as_refresh { + var r : [256]u8 = undefined; + const n : usize = t_rec(&r, 256, 0, 4217, 600, false, 0, false, false, 0, 0, 3); + var f : [22]usize = undefined; + const le : usize = line_end(&r, n, 0); + assert le == n - 1; + assert rf_split(&r, 0, le, &f) == true; + assert f[1] == 4; + assert parse_num(&r, f[0], f[1]) == 4217; + assert rf_sha_ok(&r, f[2], f[3]) == true; + assert rf_created_ok(&r, f[4], f[5]) == true; + assert rf_flag(&r, f[6], f[7]) == T_FALSE; + assert rf_flag(&r, f[10], f[11]) == T_FALSE; + assert rf_flag(&r, f[14], f[15]) == T_FALSE; + assert rf_merge_class(&r, f[16], f[17], f[18], f[19]) == M_OK; + assert rf_behind(&r, f[20], f[21], f[18], f[19]) == 1; + assert rf_classify(&r, &f) == D_REFRESH; +} + +// ---- tests: the negative controls the mutation run asked for ---- + +test a_created_stamp_is_checked_at_every_separator_and_at_its_length { + var b : [32]u8 = undefined; + var n : usize = put_str(&b, 32, 0, "2026-10-18T09:05:01Zx", 21); + assert rf_created_ok(&b, 0, n) == false; + n = put_str(&b, 32, 0, "2026/10-18T09:05:01Z", 20); + assert rf_created_ok(&b, 0, n) == false; + n = put_str(&b, 32, 0, "2026-10/18T09:05:01Z", 20); + assert rf_created_ok(&b, 0, n) == false; + n = put_str(&b, 32, 0, "2026-10-18T09-05:01Z", 20); + assert rf_created_ok(&b, 0, n) == false; + n = put_str(&b, 32, 0, "2026-10-18T09:05-01Z", 20); + assert rf_created_ok(&b, 0, n) == false; +} + +test older_ignores_the_byte_after_the_stamp { + var b : [64]u8 = undefined; + var w : usize = put_str(&b, 64, 0, "2026-10-18T09:05:01Zz", 21); + w = put_str(&b, 64, w, "2026-10-18T09:05:01Za", 21); + assert rf_older(&b, 0, 1, 21, 2) == true; + assert rf_older(&b, 21, 2, 0, 1) == false; +} + +test one_unreadable_field_alone_makes_the_record_unreadable { + var r : [256]u8 = undefined; + const n : usize = t_rec(&r, 256, 0, 720, 100, false, 0, false, false, 0, 0, 5); + assert r[45] == '2'; + assert r[73] == 'f'; + assert r[92] == 'f'; + var out : [4096]u8 = undefined; + var d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 1; + r[45] = 'x'; + d = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 0; + assert buf_has(&out, d, " 1 unreadable\n", 14) == true; + r[45] = '2'; + r[73] = 'F'; + d = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 0; + assert buf_has(&out, d, " 1 unreadable\n", 14) == true; + r[73] = 'f'; + r[92] = 'F'; + d = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 0; + assert buf_has(&out, d, " 1 unreadable\n", 14) == true; +} + +test a_record_one_field_short_is_unreadable_not_merely_undecidable { + var r : [256]u8 = undefined; + var n : usize = t_rec(&r, 256, 0, 730, 100, false, 0, false, false, 0, 0, 5); + r[n - 3] = '\n'; + n -= 2; + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 0; + assert buf_has(&out, d, " 1 unreadable\n", 14) == true; + assert buf_has(&out, d, " 0 not decidable yet, ", 22) == true; +} + +test a_blank_line_is_not_a_record { + var r : [512]u8 = undefined; + var n : usize = put_byte(&r, 512, 0, '\n'); + n = t_rec(&r, 512, n, 740, 100, false, 0, false, false, 0, 0, 5); + n = put_byte(&r, 512, n, '\n'); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 1; + assert buf_has(&out, d, " 0 unreadable\n", 14) == true; +} + +test pull_request_number_zero_is_unreadable { + var r : [256]u8 = undefined; + const n : usize = t_rec(&r, 256, 0, 0, 100, false, 0, false, false, 0, 0, 5); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 0; + assert buf_has(&out, d, " 1 unreadable\n", 14) == true; +} + +test the_513th_record_is_counted_as_unreadable_and_not_stored { + var r : [65536]u8 = undefined; + var n : usize = 0; + var i : u32 = 1; + while (i <= 513) { + n = t_rec(&r, 65536, n, i, 100, false, 0, false, false, 0, 0, 0); + i += 1; + } + assert n < 65536; + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert buf_has(&out, d, "512 already up to date, ", 24) == true; + assert buf_has(&out, d, " 1 unreadable\n", 14) == true; +} + +test the_hour_digits_decide_the_order { + var r : [1024]u8 = undefined; + var n : usize = t_rec(&r, 1024, 0, 9, 39600, false, 0, false, false, 0, 0, 1); + n = t_rec(&r, 1024, n, 3, 43200, false, 0, false, false, 0, 0, 1); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 2; + assert out[2] == '9'; + assert out[47] == '3'; +} + +test the_second_digits_decide_the_order { + var r : [1024]u8 = undefined; + var n : usize = t_rec(&r, 1024, 0, 9, 3610, false, 0, false, false, 0, 0, 1); + n = t_rec(&r, 1024, n, 3, 3659, false, 0, false, false, 0, 0, 1); + var out : [4096]u8 = undefined; + const d : usize = refresh_plan(&r, n, &out, 4096); + assert t_lines(&out, d, 'R') == 2; + assert out[2] == '9'; + assert out[47] == '3'; +} diff --git a/tools/census/quiet.txt b/tools/census/quiet.txt index 1814b5a983..ca3fcdc104 100644 --- a/tools/census/quiet.txt +++ b/tools/census/quiet.txt @@ -2,7 +2,7 @@ GATE STEPS WHOSE PASS SURVIVES THE SUBJECT GOING MISSING workflow files read 67 steps in a quiet shape 33 - named a path but not quiet 210 (--excluded prints them) + named a path but not quiet 220 (--excluded prints them) by shape: failure branch passes 14 `… 2>/dev/null … || echo PASSED` diff --git a/tools/census/shell.txt b/tools/census/shell.txt index 1d773ed370..4da213f5cb 100644 --- a/tools/census/shell.txt +++ b/tools/census/shell.txt @@ -1,11 +1,11 @@ WHICH INTERPRETER EACH GATE STEP IS HANDED TO, AND WHO SAYS SO workflow files read 67 - jobs 91 - run: steps 320 + jobs 92 + run: steps 324 who names the shell: - the runner does 299 no container, so bash -eo pipefail + the runner does 303 no container, so bash -eo pipefail a `shell:` key does 6 NOBODY 15 a container and no `shell:` key