diff --git a/.github/workflows/pr-budget.yml b/.github/workflows/pr-budget.yml new file mode 100644 index 0000000000..3332c70e38 --- /dev/null +++ b/.github/workflows/pr-budget.yml @@ -0,0 +1,39 @@ +name: PR budget (at most 20 open ready PRs per author; the excess waits as a draft) +# The rule is specs/policy/pr_budget.t27 (#10067, #10074); gen/c/policy/pr_budget.c is its t27c gen-c output +# (L2). This file is plumbing: it feeds the generated budget() one line for the event PR and one per other +# open PR of the same author, oldest first, and does what budget() prints. pull_request_target, because most +# PRs come from forks: only the base branch's gen/c/policy is checked out, no PR code runs. Nothing is ever +# closed. queen-* branches of this repository are exempt (owner, 2026-10-18). +on: + pull_request_target: + types: [opened, reopened, ready_for_review, converted_to_draft, closed] +permissions: + contents: read + pull-requests: write +jobs: + budget: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: { sparse-checkout: gen/c/policy, sparse-checkout-cone-mode: false } + - name: Draft the PR over the cap, or promote held drafts, as the generated budget() prints + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ACT: ${{ github.event.action }} + DRAFT: ${{ github.event.pull_request.draft }} + FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + N: ${{ github.event.pull_request.number }} + REF: ${{ github.event.pull_request.head.ref }} + U: ${{ github.event.pull_request.user.login }} + run: | + printf '#include "gen/c/policy/pr_budget.c"\n#include \nint main(void){static char b[1<<20],o[1<<16];budget((uint8_t*)b,fread(b,1,sizeof b,stdin),(uint8_t*)o,sizeof o-1);fputs(o,stdout);return 0;}\n' > budget.c + cc -O1 -w -o budget budget.c + k=C; case "$ACT" in opened|reopened|ready_for_review) [ "$DRAFT" = true ] || k=O;; esac + x=0; if [ "$FORK" = true ]; then x=1; fi + { echo "$k $x $N $REF"; gh pr list --state open --limit 1000 --json number,isDraft,isCrossRepository,headRefName,labels,author | jq -r --arg u "$U" --argjson n "$N" 'def l: sub("^app/";"") | sub("\\[bot\\]$";""); sort_by(.number)[] | select((.author.login | l) == ($u | l) and .number != $n) | "\(if .isDraft then (if any(.labels[]; .name == "pr-budget-held") then "H" else "D" end) else "R" end) \(if .isCrossRepository then 1 else 0 end) \(.number) \(.headRefName)"'; } > rows.txt + ./budget < rows.txt | while read -r a p; do + gh label create pr-budget-held --force -c ededed -d "Held as a draft by specs/policy/pr_budget.t27" + if [ "$a" = draft ]; then gh pr ready "$p" --undo && gh pr edit "$p" --add-label pr-budget-held && gh pr comment "$p" -b "Held as a draft by the PR budget (specs/policy/pr_budget.t27, #10074): this author already has 20 open pull requests ready for review (OPEN_CAP). Nothing is closed: this one is marked ready again when a slot frees, oldest first. Batching port waves per ring or crate needs fewer PRs." + else gh pr ready "$p" && gh pr edit "$p" --remove-label pr-budget-held && gh pr comment "$p" -b "A slot under the PR budget freed (specs/policy/pr_budget.t27, #10074): marked ready for review again."; fi || echo "::warning::pr-budget: $a $p failed" + done diff --git a/.trinity/seals/policy_PolicyPrBudget.json b/.trinity/seals/policy_PolicyPrBudget.json new file mode 100644 index 0000000000..6195cda039 --- /dev/null +++ b/.trinity/seals/policy_PolicyPrBudget.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+063da049b", + "closure": [ + { + "spec": "specs/policy/text_lines.t27", + "spec_hash": "sha256:13c90895a4a44c760cce6b3b65f72f937298633062f040f8489486108ab0c2d1" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:fdcf881cdcf97e03e270355a355fa1e075ccb720f044dd6d903a1ce39d5da5dd", + "gen_hash_rust": "sha256:59e76374ca756c3b813dc92b400c3bd9ea3a67c777cf876fc891f1a33fd272f0", + "gen_hash_verilog": "sha256:d76482675c3cfef75499c65522e0c006eed0f98bb692e60b4930b9d3e92bbded", + "gen_hash_zig": "sha256:142b6bd176982f9f316a0eb0d3a3d113a9931efcfca598641d3e2f0d250bce18", + "module": "PolicyPrBudget", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T02:19:46Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:7b5bc32463219d8e7740cf4880a5a72b94484e08fed8e326505fad158b8d81b0", + "spec_path": "specs/policy/pr_budget.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 19, + "total": 19, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/claw/README.md b/docs/claw/README.md index 8596116d99..4113655063 100644 --- a/docs/claw/README.md +++ b/docs/claw/README.md @@ -17,10 +17,10 @@ behind one front door, with the gate logic in `.t27` specs. Epic ## The inventory spec and how to regenerate it -`specs/claw/gates.t27` holds three parallel `[66]str`-shaped arrays — `NAMES` +`specs/claw/gates.t27` holds three parallel `[67]str`-shaped arrays — `NAMES` (workflow file stems), `CLASSES` (0 blocking / 1 soft / 2 reporting), `EVENTS` (trigger keys, comma-joined). Rows stay alphabetical and aligned; tests pin -the alignment, the census (61/3/2), pairwise name uniqueness, and the +the alignment, the census (62/3/2), pairwise name uniqueness, and the merge-critical rows. The class rule is mechanical, so the regeneration is too: diff --git a/gen/c/claw/gates.c b/gen/c/claw/gates.c index 7c5bb10fd1..e1c125e8d0 100644 --- a/gen/c/claw/gates.c +++ b/gen/c/claw/gates.c @@ -22,11 +22,11 @@ #define G_SOFT 1 #define G_REPORTING 2 #define G_UNKNOWN 255 -#define COUNT 66 -static const const char* NAMES[66] = { "agent-runner-docker", "auto-merge-ready-prs", "bootstrap-tests", "build-paper", "build-vivado-image", "catalog-count-gate", "cli-tri", "conflict-markers", "conformance-integrity-gate", "coq-kernel", "coq-proofs", "corpus-ratchet", "damage-negatives", "deploy-api", "documented-commands", "dupe-ratchet", "emit-bitexact-gate", "exhaustive-nightly", "fix-carries-source", "fpga-build", "gate-topology", "gf-wide-conformance", "gf16-conformance", "harness-scratch", "issue-gate", "l1-traceability", "lean-proofs", "loop-tools-gate", "needs-boundary", "oracle-nightly", "orphan-modules", "own-language", "pack-index-consistency-gate", "phi-loop-ci", "pr-dashboard", "pusher", "queen-doctor", "queen-export-push", "queen-feed-empty-bodies", "queen-feed-roadmap", "queen-feed-untested", "queen-publish", "queen-refile", "queen-watchdog", "release", "rings-rust", "sandbox-docker", "sbom", "schema-validation", "scorecard", "seal-coverage", "seal-staleness-warn", "secret-scan", "sign-release", "spec-guards", "spec-parse-ratchet", "t27b-native", "t27o-golden", "t27o-release", "untrusted-input-gate", "verilog-widths", "vivado-synth", "wasm-explorer", "withdrawn-live-gate", "xilinx7-l3-bench", "zenodo-publish" }; -static const size_t NAME_LENS[66] = { 19, 20, 15, 11, 18, 18, 7, 16, 26, 10, 10, 14, 16, 10, 19, 12, 18, 18, 18, 10, 13, 19, 16, 15, 10, 15, 11, 15, 14, 14, 14, 12, 27, 11, 12, 6, 12, 17, 23, 18, 19, 13, 12, 14, 7, 10, 14, 4, 17, 9, 13, 19, 11, 12, 11, 18, 11, 11, 12, 20, 14, 12, 13, 19, 16, 14 }; -static const uint8_t CLASSES[66] = { 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; -static const const char* EVENTS[66] = { "push,workflow_dispatch", "schedule,pull_request_target,workflow_dispatch", "push,pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "workflow_dispatch", "pull_request,workflow_dispatch", "pull_request,push", "pull_request,push,workflow_dispatch", "push,pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "push,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,workflow_dispatch", "schedule,workflow_dispatch", "pull_request,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "push,pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request_target,workflow_dispatch", "pull_request,workflow_dispatch", "pull_request,workflow_dispatch", "pull_request,push,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request", "push,pull_request,workflow_dispatch", "pull_request,workflow_dispatch", "workflow_dispatch,pull_request", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "push,schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "release", "push,pull_request,workflow_dispatch", "push,workflow_dispatch", "push,release,workflow_dispatch", "pull_request,push,workflow_dispatch", "branch_protection_rule,schedule,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "release,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "workflow_call", "push", "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "workflow_dispatch", "push,pull_request", "pull_request,push,workflow_dispatch", "workflow_dispatch,pull_request", "release,workflow_dispatch" }; +#define COUNT 67 +static const const char* NAMES[67] = { "agent-runner-docker", "auto-merge-ready-prs", "bootstrap-tests", "build-paper", "build-vivado-image", "catalog-count-gate", "cli-tri", "conflict-markers", "conformance-integrity-gate", "coq-kernel", "coq-proofs", "corpus-ratchet", "damage-negatives", "deploy-api", "documented-commands", "dupe-ratchet", "emit-bitexact-gate", "exhaustive-nightly", "fix-carries-source", "fpga-build", "gate-topology", "gf-wide-conformance", "gf16-conformance", "harness-scratch", "issue-gate", "l1-traceability", "lean-proofs", "loop-tools-gate", "needs-boundary", "oracle-nightly", "orphan-modules", "own-language", "pack-index-consistency-gate", "phi-loop-ci", "pr-budget", "pr-dashboard", "pusher", "queen-doctor", "queen-export-push", "queen-feed-empty-bodies", "queen-feed-roadmap", "queen-feed-untested", "queen-publish", "queen-refile", "queen-watchdog", "release", "rings-rust", "sandbox-docker", "sbom", "schema-validation", "scorecard", "seal-coverage", "seal-staleness-warn", "secret-scan", "sign-release", "spec-guards", "spec-parse-ratchet", "t27b-native", "t27o-golden", "t27o-release", "untrusted-input-gate", "verilog-widths", "vivado-synth", "wasm-explorer", "withdrawn-live-gate", "xilinx7-l3-bench", "zenodo-publish" }; +static const size_t NAME_LENS[67] = { 19, 20, 15, 11, 18, 18, 7, 16, 26, 10, 10, 14, 16, 10, 19, 12, 18, 18, 18, 10, 13, 19, 16, 15, 10, 15, 11, 15, 14, 14, 14, 12, 27, 11, 9, 12, 6, 12, 17, 23, 18, 19, 13, 12, 14, 7, 10, 14, 4, 17, 9, 13, 19, 11, 12, 11, 18, 11, 11, 12, 20, 14, 12, 13, 19, 16, 14 }; +static const uint8_t CLASSES[67] = { 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +static const const char* EVENTS[67] = { "push,workflow_dispatch", "schedule,pull_request_target,workflow_dispatch", "push,pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "workflow_dispatch", "pull_request,workflow_dispatch", "pull_request,push", "pull_request,push,workflow_dispatch", "push,pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "push,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,workflow_dispatch", "schedule,workflow_dispatch", "pull_request,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "push,pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request_target,workflow_dispatch", "pull_request,workflow_dispatch", "pull_request,workflow_dispatch", "pull_request,push,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request", "push,pull_request,workflow_dispatch", "pull_request,workflow_dispatch", "pull_request_target", "workflow_dispatch,pull_request", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "push,schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "release", "push,pull_request,workflow_dispatch", "push,workflow_dispatch", "push,release,workflow_dispatch", "pull_request,push,workflow_dispatch", "branch_protection_rule,schedule,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "release,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "workflow_call", "push", "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "workflow_dispatch", "push,pull_request", "pull_request,push,workflow_dispatch", "workflow_dispatch,pull_request", "release,workflow_dispatch" }; #define P_OK 0 #define P_SPEC_ONLY 1 #define P_DIR_ONLY 2 @@ -73,7 +73,7 @@ size_t claw_coverage_report(uint8_t* out, size_t out_cap); ------------------------------------------------------- */ uint8_t class_of(const char* name, size_t name_len) { - for (size_t i = 0; i < 66; i++) { + for (size_t i = 0; i < 67; i++) { if (str_eq(NAMES[i], NAME_LENS[i], name, name_len)) { return CLASSES[i]; } @@ -86,7 +86,7 @@ bool is_blocking(const char* name, size_t name_len) { } const char* events_of(const char* name, size_t name_len) { - for (size_t i = 0; i < 66; i++) { + for (size_t i = 0; i < 67; i++) { if (str_eq(NAMES[i], NAME_LENS[i], name, name_len)) { return EVENTS[i]; } @@ -96,7 +96,7 @@ const char* events_of(const char* name, size_t name_len) { uint32_t count_class(uint8_t c) { uint32_t n = 0; - for (size_t i = 0; i < 66; i++) { + for (size_t i = 0; i < 67; i++) { if ((CLASSES[i] == c)) { n += 1; } @@ -105,7 +105,7 @@ uint32_t count_class(uint8_t c) { } bool every_row_filled(void) { - for (size_t i = 0; i < 66; i++) { + for (size_t i = 0; i < 67; i++) { if ((NAMES[i] == "")) { return false; } @@ -132,8 +132,8 @@ bool str_eq(const char* a, size_t alen, const char* b, size_t blen) { } bool all_names_unique(void) { - for (size_t i = 0; i < 66; i++) { - for (size_t j = (i + 1); j < 66; j++) { + for (size_t i = 0; i < 67; i++) { + for (size_t j = (i + 1); j < 67; j++) { if (str_eq(NAMES[i], NAME_LENS[i], NAMES[j], NAME_LENS[j])) { return false; } @@ -143,7 +143,7 @@ bool all_names_unique(void) { } bool all_classes_known(void) { - for (size_t i = 0; i < 66; i++) { + for (size_t i = 0; i < 67; i++) { if ((CLASSES[i] != G_BLOCKING)) { if ((CLASSES[i] != G_SOFT)) { if ((CLASSES[i] != G_REPORTING)) { @@ -194,7 +194,7 @@ bool dir_has(uint8_t* dir_text, size_t dir_len, const char* name, size_t name_le } bool line_in_names(uint8_t* dir_text, size_t start, size_t len) { - for (size_t i = 0; i < 66; i++) { + for (size_t i = 0; i < 67; i++) { if (line_eq_name(dir_text, start, len, NAMES[i], NAME_LENS[i])) { return true; } @@ -295,7 +295,7 @@ int32_t claw_status(uint8_t* dir_text, size_t dir_len, uint8_t* out, size_t out_ int32_t problems = 0; w = put_str(out, out_cap, w, AUDIT_HEAD, HEAD_LEN); w = put_str(out, out_cap, w, "\n", 1); - for (size_t i = 0; i < 66; i++) { + for (size_t i = 0; i < 67; i++) { if ((dir_has(dir_text, dir_len, NAMES[i], NAME_LENS[i]) == false)) { w = put_str(out, out_cap, w, LINE_PREFIX[P_SPEC_ONLY], PREFIX_LENS[P_SPEC_ONLY]); w = put_str(out, out_cap, w, NAMES[i], NAME_LENS[i]); @@ -347,7 +347,7 @@ size_t claw_coverage_report(uint8_t* out, size_t out_cap) { w = put_str(out, out_cap, w, " blocking=", 10); size_t shown = 0; size_t i = 0; - while ((i < 66)) { + while ((i < 67)) { if ((shown < 3)) { if ((CLASSES[i] == c)) { if ((shown == 0)) { @@ -412,15 +412,15 @@ void test_critical_gates_present_and_blocking(void) { assert((is_blocking(c, 12) == true)); } -void test_class_census_61_3_2(void) { - assert((count_class(G_BLOCKING) == 61)); +void test_class_census_62_3_2(void) { + assert((count_class(G_BLOCKING) == 62)); assert((count_class(G_SOFT) == 3)); assert((count_class(G_REPORTING) == 2)); } void test_rows_stay_aligned_name_class_events(void) { const char* want1 = "auto-merge-ready-prs"; - const char* want41 = "queen-publish"; + const char* want42 = "queen-publish"; const char* want29 = "oracle-nightly"; const char* qp = "queen-publish"; const char* ol = "own-language"; @@ -428,8 +428,8 @@ void test_rows_stay_aligned_name_class_events(void) { const char* ev_ol = "pull_request"; assert((NAMES[1] == want1)); assert((CLASSES[1] == G_REPORTING)); - assert((NAMES[41] == want41)); - assert((CLASSES[41] == G_REPORTING)); + assert((NAMES[42] == want42)); + assert((CLASSES[42] == G_REPORTING)); assert((NAMES[29] == want29)); assert((CLASSES[29] == G_SOFT)); assert((events_of(qp, 13) == ev_qp)); @@ -455,7 +455,7 @@ void test_reporting_gates_never_block_a_merge(void) { void test_audit_clean_when_directory_matches_inventory(void) { uint8_t dir[8192] = {0}; size_t w = 0; - for (size_t i = 0; i < 66; i++) { + for (size_t i = 0; i < 67; i++) { w = put_str(&dir, 8192, w, NAMES[i], NAME_LENS[i]); w = put_str(&dir, 8192, w, "\n", 1); } @@ -469,7 +469,7 @@ void test_audit_clean_when_directory_matches_inventory(void) { void test_audit_spec_only_when_a_workflow_file_is_missing(void) { uint8_t dir[8192] = {0}; size_t w = 0; - for (size_t i = 1; i < 66; i++) { + for (size_t i = 1; i < 67; i++) { w = put_str(&dir, 8192, w, NAMES[i], NAME_LENS[i]); w = put_str(&dir, 8192, w, "\n", 1); } @@ -483,7 +483,7 @@ void test_audit_spec_only_when_a_workflow_file_is_missing(void) { void test_audit_dir_only_for_an_unknown_workflow(void) { uint8_t dir[8192] = {0}; size_t w = 0; - for (size_t i = 0; i < 66; i++) { + for (size_t i = 0; i < 67; i++) { w = put_str(&dir, 8192, w, NAMES[i], NAME_LENS[i]); w = put_str(&dir, 8192, w, "\n", 1); } @@ -496,7 +496,7 @@ void test_audit_dir_only_for_an_unknown_workflow(void) { void test_audit_ignores_blank_lines_in_the_directory(void) { uint8_t dir[8192] = {0}; size_t w = 0; - for (size_t i = 0; i < 66; i++) { + for (size_t i = 0; i < 67; i++) { w = put_str(&dir, 8192, w, NAMES[i], NAME_LENS[i]); w = put_str(&dir, 8192, w, "\n", 1); } @@ -519,7 +519,7 @@ void test_audit_prefixes_name_the_problem_kind(void) { void test_every_row_round_trips_through_the_name_lookup(void) { size_t i = 0; - while ((i < 66)) { + while ((i < 67)) { assert((class_of(NAMES[i], NAME_LENS[i]) == CLASSES[i])); if ((CLASSES[i] == G_BLOCKING)) { assert((is_blocking(NAMES[i], NAME_LENS[i]) == true)); @@ -543,11 +543,11 @@ void test_coverage_report_carries_every_class(void) { assert((buf_has(&out, w, "G_BLOCKING", 10) == true)); assert((buf_has(&out, w, "G_SOFT", 6) == true)); assert((buf_has(&out, w, "G_REPORTING", 11) == true)); - assert((buf_has(&out, w, "n=61", 4) == true)); + assert((buf_has(&out, w, "n=62", 4) == true)); assert((buf_has(&out, w, "n=3", 3) == true)); assert((buf_has(&out, w, "n=2", 3) == true)); assert((buf_has(&out, w, "n=0", 3) == true)); - assert((buf_has(&out, w, "n=62", 4) == false)); + assert((buf_has(&out, w, "n=63", 4) == false)); assert((buf_has(&out, w, "G_UNKNOWN", 9) == true)); } @@ -564,7 +564,7 @@ int main(void) { test_known_classes_only(); test_no_duplicate_names_pairwise(); test_critical_gates_present_and_blocking(); - test_class_census_61_3_2(); + test_class_census_62_3_2(); test_rows_stay_aligned_name_class_events(); test_unknown_name_is_unknown_and_not_blocking(); test_nightly_pair_contrast(); diff --git a/gen/c/policy/pr_budget.c b/gen/c/policy/pr_budget.c new file mode 100644 index 0000000000..571e9a6a4d --- /dev/null +++ b/gen/c/policy/pr_budget.c @@ -0,0 +1,540 @@ +/* ============================================================================ + Generated from t27 spec: PolicyPrBudget + 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 POLICYPRBUDGET_H +#define POLICYPRBUDGET_H + +/* ------------------------------------------------------- + Constants + ------------------------------------------------------- */ + +#define OPEN_CAP 20 +#define TRI_PER_SPEC 27 +#define CHECKS_PER_PR 29 +#define PUSH_RUNS_PER_MERGE 19 +#define READY 0 +#define HOLD_DRAFT 1 +#define PORT_TITLE "spec(port)" +#define PORT_TITLE_LEN 10 +#define PORT_BRANCH_ZCODE "zcode-port-" +#define PORT_BRANCH_ZCODE_LEN 11 +#define PORT_BRANCH_CR_CHAT "port/cr-chat-" +#define PORT_BRANCH_CR_CHAT_LEN 13 +#define M_OPEN_FEDOROV 128 +#define M_OPEN_GHASHTAG 162 +#define M_OPEN_GHASHTAG_QUEEN 131 +#define M_RATE_LOW_PER_HOUR 6 +#define M_RATE_HIGH_PER_HOUR 9 +#define M_RUNS_SAMPLE 1000 +#define M_RUNS_MASTER_PUSH 732 +#define M_RUNS_FEDOROV_PR 76 +#define M_SAMPLE_MINUTES 25 +#define M_QUEUED 51 +#define M_QUEUED_FEDOROV 29 +#define EXEMPT_BRANCH "queen-" +#define EXEMPT_BRANCH_LEN 6 +#define ACT_DRAFT "draft " +#define ACT_READY "ready " +#define ACT_LEN 6 + +/* ------------------------------------------------------- + Function prototypes + ------------------------------------------------------- */ + +bool has_prefix(uint8_t* buf, size_t n, uint8_t* p, size_t pn); +bool is_port_pr(uint8_t* title, size_t tn, uint8_t* branch, size_t bn); +uint32_t wave_prs(uint32_t files, uint32_t groups, bool batched); +uint32_t wave_runs(uint32_t prs); +uint32_t wave_credit(uint32_t files); +uint8_t admit(uint32_t open_ready, uint32_t cap); +uint32_t held_as_draft(uint32_t open_ready, uint32_t opened, uint32_t cap); +uint32_t promoted(uint32_t open_ready, uint32_t drafts, uint32_t cap); +uint32_t hours_to_fill(uint32_t cap, uint32_t rate); +uint32_t permille(uint32_t part, uint32_t whole); +bool has_prefix_at(uint8_t* buf, size_t s, size_t e, uint8_t* p, size_t pn); +bool is_exempt_at(uint8_t* buf, size_t s, size_t e, bool fork); +bool is_exempt(uint8_t* branch, size_t bn, bool fork); +bool is_digit(uint8_t c); +size_t digits_end(uint8_t* buf, size_t s, size_t e); +bool row_ok(uint8_t* buf, size_t s, size_t e); +uint32_t row_number(uint8_t* buf, size_t s, size_t e); +bool row_exempt(uint8_t* buf, size_t s, size_t e); +bool row_counts(uint8_t* buf, size_t s, size_t e, uint8_t st); +uint32_t count_state(uint8_t* buf, size_t n, uint8_t st); +size_t put(uint8_t* out, size_t w, size_t cap, uint8_t c); +size_t put_act(uint8_t* out, size_t w, size_t cap, uint8_t* act, uint32_t v); +uint32_t budget(uint8_t* buf, size_t n, uint8_t* out, size_t cap); +uint32_t budget_with(uint8_t* buf, size_t n, uint8_t* out, size_t cap, uint32_t limit); +size_t line_end(uint8_t* buf, size_t s, size_t n); + +/* ------------------------------------------------------- + Function implementations + ------------------------------------------------------- */ + +bool has_prefix(uint8_t* buf, size_t n, uint8_t* p, size_t pn) { + if ((pn > n)) { + return false; + } + size_t i = 0; + while ((i < pn)) { + if ((buf[i] != p[i])) { + return false; + } + i += 1; + } + return true; +} + +bool is_port_pr(uint8_t* title, size_t tn, uint8_t* branch, size_t bn) { + if (has_prefix(title, tn, PORT_TITLE, PORT_TITLE_LEN)) { + return true; + } + if (has_prefix(branch, bn, PORT_BRANCH_ZCODE, PORT_BRANCH_ZCODE_LEN)) { + return true; + } + return has_prefix(branch, bn, PORT_BRANCH_CR_CHAT, PORT_BRANCH_CR_CHAT_LEN); +} + +uint32_t wave_prs(uint32_t files, uint32_t groups, bool batched) { + if (batched) { + return groups; + } + return files; +} + +uint32_t wave_runs(uint32_t prs) { + return ((prs * CHECKS_PER_PR) + (prs * PUSH_RUNS_PER_MERGE)); +} + +uint32_t wave_credit(uint32_t files) { + return (files * TRI_PER_SPEC); +} + +uint8_t admit(uint32_t open_ready, uint32_t cap) { + if ((open_ready < cap)) { + return READY; + } + return HOLD_DRAFT; +} + +uint32_t held_as_draft(uint32_t open_ready, uint32_t opened, uint32_t cap) { + if ((open_ready >= cap)) { + return opened; + } + uint32_t free = (cap - open_ready); + if ((opened <= free)) { + return 0; + } + return (opened - free); +} + +uint32_t promoted(uint32_t open_ready, uint32_t drafts, uint32_t cap) { + if ((open_ready >= cap)) { + return 0; + } + uint32_t free = (cap - open_ready); + if ((drafts <= free)) { + return drafts; + } + return free; +} + +uint32_t hours_to_fill(uint32_t cap, uint32_t rate) { + return (((cap + rate) - 1) / rate); +} + +uint32_t permille(uint32_t part, uint32_t whole) { + return ((part * 1000) / whole); +} + +bool has_prefix_at(uint8_t* buf, size_t s, size_t e, uint8_t* p, size_t pn) { + if (((s + pn) > e)) { + return false; + } + size_t i = 0; + while ((i < pn)) { + if ((buf[(s + i)] != p[i])) { + return false; + } + i += 1; + } + return true; +} + +bool is_exempt_at(uint8_t* buf, size_t s, size_t e, bool fork) { + if (fork) { + return false; + } + return has_prefix_at(buf, s, e, EXEMPT_BRANCH, EXEMPT_BRANCH_LEN); +} + +bool is_exempt(uint8_t* branch, size_t bn, bool fork) { + return is_exempt_at(branch, 0, bn, fork); +} + +bool is_digit(uint8_t c) { + return ((c >= '0') && (c <= '9')); +} + +size_t digits_end(uint8_t* buf, size_t s, size_t e) { + size_t i = (s + 4); + while (((i < e) && is_digit(buf[i]))) { + i += 1; + } + return i; +} + +bool row_ok(uint8_t* buf, size_t s, size_t e) { + if ((e < (s + 7))) { + return false; + } + if (((buf[(s + 1)] != ' ') || (buf[(s + 3)] != ' '))) { + return false; + } + if (((buf[(s + 2)] != '0') && (buf[(s + 2)] != '1'))) { + return false; + } + size_t d = digits_end(buf, s, e); + if (((d == (s + 4)) || (d > (s + 13)))) { + return false; + } + return (((d + 1) < e) && (buf[d] == ' ')); +} + +uint32_t row_number(uint8_t* buf, size_t s, size_t e) { + size_t d = digits_end(buf, s, e); + uint32_t v = 0; + size_t i = (s + 4); + while ((i < d)) { + v = ((v * 10) + (uint32_t)((buf[i] - '0'))); + i += 1; + } + return v; +} + +bool row_exempt(uint8_t* buf, size_t s, size_t e) { + return is_exempt_at(buf, (digits_end(buf, s, e) + 1), e, (buf[(s + 2)] == '1')); +} + +bool row_counts(uint8_t* buf, size_t s, size_t e, uint8_t st) { + if ((row_ok(buf, s, e) == false)) { + return false; + } + if ((buf[s] != st)) { + return false; + } + return (row_exempt(buf, s, e) == false); +} + +uint32_t count_state(uint8_t* buf, size_t n, uint8_t st) { + uint32_t c = 0; + size_t s = (line_end(buf, 0, n) + 1); + while ((s < n)) { + size_t e = line_end(buf, s, n); + if (row_counts(buf, s, e, st)) { + c += 1; + } + s = (e + 1); + } + return c; +} + +size_t put(uint8_t* out, size_t w, size_t cap, uint8_t c) { + if ((w < cap)) { + out[w] = c; + } + return (w + 1); +} + +size_t put_act(uint8_t* out, size_t w, size_t cap, uint8_t* act, uint32_t v) { + size_t p = w; + size_t i = 0; + while ((i < ACT_LEN)) { + p = put(out, p, cap, act[i]); + i += 1; + } + uint32_t d = 1; + while (((v / d) >= 10)) { + d = (d * 10); + } + while ((d > 0)) { + uint8_t c = '0'; + uint32_t r = ((v / d) % 10); + while ((r > 0)) { + c += 1; + r -= 1; + } + p = put(out, p, cap, c); + d = (d / 10); + } + return put(out, p, cap, '\n'); +} + +uint32_t budget(uint8_t* buf, size_t n, uint8_t* out, size_t cap) { + return budget_with(buf, n, out, cap, OPEN_CAP); +} + +uint32_t budget_with(uint8_t* buf, size_t n, uint8_t* out, size_t cap, uint32_t limit) { + size_t e0 = line_end(buf, 0, n); + if ((row_ok(buf, 0, e0) == false)) { + return 0; + } + uint32_t ready = count_state(buf, n, 'R'); + if ((buf[0] == 'O')) { + if (row_exempt(buf, 0, e0)) { + return 0; + } + if ((admit(ready, limit) == READY)) { + return 0; + } + if ((put_act(out, 0, cap, ACT_DRAFT, row_number(buf, 0, e0)) > cap)) { + return 1; + } + return 1; + } + if ((buf[0] != 'C')) { + return 0; + } + uint32_t k = promoted(ready, count_state(buf, n, 'H'), limit); + uint32_t acts = 0; + size_t w = 0; + size_t s = (e0 + 1); + while (((s < n) && (acts < k))) { + size_t e = line_end(buf, s, n); + if (row_counts(buf, s, e, 'H')) { + w = put_act(out, w, cap, ACT_READY, row_number(buf, s, e)); + acts += 1; + } + s = (e + 1); + } + return acts; +} + +size_t line_end(uint8_t* buf, size_t s, size_t n) { + size_t e = s; + while (((e < n) && (buf[e] != '\n'))) { + e += 1; + } + return e; +} + +/* ------------------------------------------------------- + Invariants (compile-time assertions) + ------------------------------------------------------- */ + +/* invariant: credit_does_not_depend_on_pr_count */ +/* invariant credit_does_not_depend_on_pr_count is not a C constant expression: (wave_credit(1) == TRI_PER_SPEC) */ +/* invariant credit_does_not_depend_on_pr_count is not a C constant expression: (wave_credit(20) == (20 * wave_credit(1))) */ +/* invariant credit_does_not_depend_on_pr_count is not a C constant expression: (wave_prs(20, 1, true) < wave_prs(20, 1, false)) */ + +/* invariant: batching_never_costs_more_runs */ +/* invariant batching_never_costs_more_runs is not a C constant expression: (wave_runs(wave_prs(20, 20, true)) == wave_runs(wave_prs(20, 20, false))) */ +/* invariant batching_never_costs_more_runs is not a C constant expression: (wave_runs(wave_prs(20, 1, true)) <= wave_runs(wave_prs(20, 1, false))) */ +/* invariant batching_never_costs_more_runs is not a C constant expression: (wave_runs(2) == (2 * wave_runs(1))) */ +/* invariant batching_never_costs_more_runs is not a C constant expression: (wave_runs(0) == 0) */ + +/* invariant: held_and_ready_add_up_and_never_exceed_the_cap */ +/* invariant held_and_ready_add_up_and_never_exceed_the_cap is not a C constant expression: ((15 + (10 - held_as_draft(15, 10, OPEN_CAP))) == OPEN_CAP) */ +/* invariant held_and_ready_add_up_and_never_exceed_the_cap is not a C constant expression: ((0 + (20 - held_as_draft(0, 20, OPEN_CAP))) == OPEN_CAP) */ +/* invariant held_and_ready_add_up_and_never_exceed_the_cap is not a C constant expression: ((17 + promoted(17, 108, OPEN_CAP)) == OPEN_CAP) */ +_Static_assert((READY != HOLD_DRAFT), "invariant: held_and_ready_add_up_and_never_exceed_the_cap"); + + +/* ------------------------------------------------------- + Tests + ------------------------------------------------------- */ + +void test_port_prs_are_named_by_title_or_branch(void) { + assert((is_port_pr("spec(port): port specs/port/trios-railway/crates/trios-igla-race/src/race.t27", 77, "x", 1) == true)); + assert((is_port_pr("chore: x", 8, "zcode-port-9776", 15) == true)); + assert((is_port_pr("chore: x", 8, "port/cr-chat-12", 15) == true)); +} + +void test_other_prs_are_not_port_prs(void) { + assert((is_port_pr("spec(t27f): spec_graph_view", 27, "claude/frosty-knuth-6faee5", 26) == false)); + assert((is_port_pr("fix(t27b): fold a constant tie-off", 34, "queen-10046", 11) == false)); + assert((is_port_pr("spec(ports)", 11, "zcode-port", 10) == false)); + assert((is_port_pr("Spec(port): x", 13, "port/cr-chat", 12) == false)); + assert((is_port_pr("spec(", 5, "port/", 5) == false)); +} + +void test_a_twenty_file_wave_costs_960_runs_unbatched_and_48_batched(void) { + uint32_t prs_one_per_file = wave_prs(20, 1, false); + uint32_t prs_batched = wave_prs(20, 1, true); + assert((prs_one_per_file == 20)); + assert((prs_batched == 1)); + assert((wave_runs(prs_one_per_file) == ((20 * 29) + (20 * 19)))); + assert((wave_runs(prs_one_per_file) == 960)); + assert((wave_runs(prs_batched) == 48)); + assert(((wave_runs(prs_one_per_file) / wave_runs(prs_batched)) == 20)); +} + +void test_batching_keeps_the_credit(void) { + assert((wave_credit(20) == 540)); + assert((wave_credit(20) == (20 * TRI_PER_SPEC))); +} + +void test_a_wave_over_three_crates_is_three_prs(void) { + assert((wave_prs(20, 3, true) == 3)); + assert((wave_runs(wave_prs(20, 3, true)) == 144)); +} + +void test_the_cap_admits_up_to_20_and_holds_the_21st(void) { + assert((admit(0, OPEN_CAP) == READY)); + assert((admit(19, OPEN_CAP) == READY)); + assert((admit(20, OPEN_CAP) == HOLD_DRAFT)); + assert((admit(21, OPEN_CAP) == HOLD_DRAFT)); +} + +void test_the_excess_is_held_as_drafts(void) { + assert((held_as_draft(0, 20, OPEN_CAP) == 0)); + assert((held_as_draft(15, 10, OPEN_CAP) == 5)); + assert((held_as_draft(20, 3, OPEN_CAP) == 3)); + assert((held_as_draft(25, 1, OPEN_CAP) == 1)); +} + +void test_drafts_are_promoted_as_slots_free(void) { + assert((promoted(20, 108, OPEN_CAP) == 0)); + assert((promoted(17, 108, OPEN_CAP) == 3)); + assert((promoted(10, 4, OPEN_CAP) == 4)); + assert((promoted(0, 0, OPEN_CAP) == 0)); +} + +void test_measured_2026_10_17_open_prs_against_the_cap(void) { + assert((held_as_draft(0, M_OPEN_FEDOROV, OPEN_CAP) == 108)); + assert((held_as_draft(0, M_OPEN_GHASHTAG, OPEN_CAP) == 142)); + assert(((M_OPEN_GHASHTAG_QUEEN * 2) > M_OPEN_GHASHTAG)); + assert(((M_OPEN_GHASHTAG - M_OPEN_GHASHTAG_QUEEN) == 31)); + assert((held_as_draft(0, (M_OPEN_GHASHTAG - M_OPEN_GHASHTAG_QUEEN), OPEN_CAP) == 11)); +} + +void test_measured_2026_10_17_rate_fills_the_cap_in_hours(void) { + assert(((M_RATE_LOW_PER_HOUR * 24) == 144)); + assert(((M_RATE_HIGH_PER_HOUR * 24) == 216)); + assert((hours_to_fill(OPEN_CAP, M_RATE_HIGH_PER_HOUR) == 3)); + assert((hours_to_fill(OPEN_CAP, M_RATE_LOW_PER_HOUR) == 4)); +} + +void test_measured_2026_10_17_actions_runs(void) { + assert((permille(M_RUNS_MASTER_PUSH, M_RUNS_SAMPLE) == 732)); + assert(((M_RUNS_MASTER_PUSH / PUSH_RUNS_PER_MERGE) == 38)); + assert((permille(M_RUNS_FEDOROV_PR, M_RUNS_SAMPLE) == 76)); + assert((permille(M_QUEUED_FEDOROV, M_QUEUED) == 568)); + assert(((M_QUEUED_FEDOROV * 2) > M_QUEUED)); +} + +void test_queen_branches_here_are_exempt_and_forks_are_not(void) { + assert((is_exempt("queen-10046", 11, false) == true)); + assert((is_exempt("queen-10046", 11, true) == false)); + assert((is_exempt("queen", 5, false) == false)); + assert((is_exempt("Queen-1", 7, false) == false)); + assert((is_exempt("zcode-port-9776", 15, false) == false)); +} + +void test_a_row_reads_or_is_skipped(void) { + assert((row_ok("O 1 10070 port-x", 0, 16) == true)); + assert((row_number("O 1 10070 port-x", 0, 16) == 10070)); + assert((row_ok("O 2 1 a-1", 0, 9) == false)); + assert((row_ok("O 1 x port", 0, 10) == false)); + assert((row_ok("O 1 1 ", 0, 6) == false)); + assert((row_ok("O 1 1", 0, 5) == false)); + assert((row_ok("O 1 1234567890 a", 0, 16) == false)); + assert((row_ok("O 1 123456789 a", 0, 15) == true)); + assert((row_number("O 1 123456789 a", 0, 15) == 123456789)); + assert((row_exempt("R 0 1 queen-1", 0, 13) == true)); + assert((row_exempt("R 1 1 queen-1", 0, 13) == false)); +} + +void test_put_act_writes_the_number_in_decimal(void) { + uint8_t out[32] = {0}; + assert((put_act(&out, 0, 32, ACT_DRAFT, 10070) == 12)); + assert((has_prefix(&out, 12, "draft 10070\n", 12) == true)); + assert((put_act(&out, 0, 32, ACT_READY, 0) == 8)); + assert((has_prefix(&out, 8, "ready 0\n", 8) == true)); + assert((put_act(&out, 0, 32, ACT_READY, 999999999) == 16)); + assert((has_prefix(&out, 16, "ready 999999999\n", 16) == true)); + uint8_t tiny[4] = {0}; + assert((put_act(&tiny, 0, 4, ACT_DRAFT, 7) == 8)); +} + +void test_the_pr_over_the_cap_is_drafted(void) { + uint8_t out[64] = {0}; + assert((budget_with("O 1 10070 port-x\nR 1 10001 a-1\nR 1 10002 a-2\n", 45, &out, 64, 2) == 1)); + assert((has_prefix(&out, 12, "draft 10070\n", 12) == true)); + assert((budget_with("O 1 10070 port-x\nR 1 10001 a-1\n", 31, &out, 64, 2) == 0)); +} + +void test_the_cap_of_20_holds_the_21st_and_admits_the_20th(void) { + uint8_t out[64] = {0}; + const uint8_t twenty[] = "O 1 99 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\n"; + assert((budget(twenty, 169, &out, 64) == 1)); + assert((has_prefix(&out, 9, "draft 99\n", 9) == true)); + assert((budget(twenty, 161, &out, 64) == 0)); +} + +void test_bees_are_never_drafted_and_never_counted(void) { + uint8_t out[64] = {0}; + assert((budget_with("O 0 10070 queen-10070\nR 0 1 x-1\nR 0 2 x-2\n", 42, &out, 64, 2) == 0)); + assert((budget_with("O 1 10070 queen-1\nR 1 1 x-1\nR 1 2 x-2\n", 38, &out, 64, 2) == 1)); + assert((budget_with("O 0 10070 fix-a\nR 0 1 queen-1\nR 0 2 queen-2\nR 0 3 b-3\n", 54, &out, 64, 2) == 0)); +} + +void test_a_freed_slot_promotes_the_oldest_held_drafts(void) { + uint8_t out[64] = {0}; + assert((budget_with("C 1 9 port-9\nR 1 1 a-1\nH 1 5 a-5\nD 1 6 a-6\nH 1 7 a-7\nH 1 8 a-8\n", 63, &out, 64, 3) == 2)); + assert((has_prefix(&out, 16, "ready 5\nready 7\n", 16) == true)); + assert((budget_with("C 1 9 port-9\nR 1 1 a-1\nH 1 5 a-5\nD 1 6 a-6\nH 1 7 a-7\nH 1 8 a-8\n", 63, &out, 64, 1) == 0)); +} + +void test_rows_that_do_not_read_are_skipped(void) { + uint8_t out[64] = {0}; + assert((budget_with("O 1 x port\nR 1 1 a\nR 1 2 b\n", 27, &out, 64, 1) == 0)); + assert((budget_with("X 1 1 a-1\nR 1 2 b-2\n", 20, &out, 64, 1) == 0)); + assert((budget_with("O 1 7 a-7\nR 1 1 a-1\nR 2 2 b-2\nR 1 3\nR 1 x c-4\n", 46, &out, 64, 2) == 0)); + assert((budget_with("O 1 7 a-7\nR 1 1 a-1\nR 2 2 b-2\nR 1 3\nR 1 x c-4\n", 46, &out, 64, 1) == 1)); + assert((budget_with("O 1 99 b", 8, &out, 64, 0) == 1)); +} + + +/* ------------------------------------------------------- + Test runner (compile with -DT27_TEST_MAIN to execute) + ------------------------------------------------------- */ + +#ifdef T27_TEST_MAIN +#include +int main(void) { + test_port_prs_are_named_by_title_or_branch(); + test_other_prs_are_not_port_prs(); + test_a_twenty_file_wave_costs_960_runs_unbatched_and_48_batched(); + test_batching_keeps_the_credit(); + test_a_wave_over_three_crates_is_three_prs(); + test_the_cap_admits_up_to_20_and_holds_the_21st(); + test_the_excess_is_held_as_drafts(); + test_drafts_are_promoted_as_slots_free(); + test_measured_2026_10_17_open_prs_against_the_cap(); + test_measured_2026_10_17_rate_fills_the_cap_in_hours(); + test_measured_2026_10_17_actions_runs(); + test_queen_branches_here_are_exempt_and_forks_are_not(); + test_a_row_reads_or_is_skipped(); + test_put_act_writes_the_number_in_decimal(); + test_the_pr_over_the_cap_is_drafted(); + test_the_cap_of_20_holds_the_21st_and_admits_the_20th(); + test_bees_are_never_drafted_and_never_counted(); + test_a_freed_slot_promotes_the_oldest_held_drafts(); + test_rows_that_do_not_read_are_skipped(); + printf("All %d tests passed.\n", 19); + return 0; +} +#endif /* T27_TEST_MAIN */ + +#endif /* POLICYPRBUDGET_H */ diff --git a/scripts/trinity-pellis-pipeline/fix_bugs.py b/scripts/trinity-pellis-pipeline/fix_bugs.py deleted file mode 100644 index 65a89d974e..0000000000 --- a/scripts/trinity-pellis-pipeline/fix_bugs.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -"""Fix BUG-1 and BUG-2 in G2_ALPHA_S_PHI_FRAMEWORK_V0.9.tex - -BUG-1: Poisson exact p-value uses 10^{-53} instead of 10^{-4} -BUG-2: CKM unitarity formula has approx 1$ instead of = 1$ -""" - -import sys -from pathlib import Path - -tex_file = Path(__file__).parent.parent.parent / "research" / "trinity-pellis-paper" / "G2_ALPHA_S_PHI_FRAMEWORK_V0.9.tex" - -with open(tex_file, 'r') as f: - content = f.read() - -print(f"Read {len(content)} lines") - -# Fix BUG-1: Change 10^{-53} to 10^{-4} in summary table -# Two occurrences: line 167 (Poisson exact section) and line 180 (summary table) - -content = content.replace("1.47 \\times 10^{-53}", "1.47 \\times 10^{-4}") - -# Fix BUG-2: Change CKM unitarity approx 1$ to = 1$ -# Find the CKM unitarity formula in Block Permutation section -content = content.replace("|V_{ud}|^2 + |V_{us}|^2 + |V_{ub}|^2 \\approx 1$", - "|V_{ud}|^2 + |V_{us}|^2 + |V_{ub}|^2 = 1$") - -with open(tex_file, 'w') as f: - f.write(content) - -print(f"Fixed BUG-1 and BUG-2") -print(f"Lines modified: {len(content) - len(content.split(chr(10)))}") - -# Verify fixes -print("\nVerification:") -if "1.47 \\times 10^{-4}" in content: - print(" ✓ BUG-1 fixed: 10^{-53} → 10^{-4}") -else: - print(" ✗ BUG-1 not found") - -if "|V_{ud}|^2 + |V_{us}|^2 + |V_{ub}|^2 = 1$" in content: - print(" ✓ BUG-2 fixed: CKM unitarity") -else: - print(" ✗ BUG-2 not found") diff --git a/scripts/trinity-pellis-pipeline/fix_bugs_v2.py b/scripts/trinity-pellis-pipeline/fix_bugs_v2.py deleted file mode 100644 index 2cf1175d48..0000000000 --- a/scripts/trinity-pellis-pipeline/fix_bugs_v2.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -"""Fix BUG-1 and BUG-2 in G2_ALPHA_S_PHI_FRAMEWORK_V0.9.tex - -BUG-1: Poisson exact p-value uses 10^{-53} instead of 10^{-4} -BUG-2: CKM unitarity formula has approx 1$ instead of = 1$ (appears in TWO places) -""" - -import sys -from pathlib import Path - -tex_file = Path(__file__).parent.parent.parent / "research" / "trinity-pellis-paper" / "G2_ALPHA_S_PHI_FRAMEWORK_V0.9.tex" - -with open(tex_file, 'r') as f: - content = f.read() - -print(f"Read {len(content)} lines") - -# Fix BUG-1: Change 10^{-53} to 10^{-4} in summary table (line 180) -content = content.replace("1.47 \\times 10^{-53}", "1.47 \\times 10^{-4}") - -# Fix BUG-2: Change approx 1$ to = 1$ in BOTH places -# This requires finding the exact formula occurrences and replacing them - -# First, let's find all occurrences of the buggy CKM unitarity formula -buggy_formula = "$|V_{ud}|^2 + |V_{us}|^2 + |V_{ub}|^2 \\approx 1$" - -# Count occurrences before replacement -count_before = content.count(buggy_formula) -print(f"Found {count_before} occurrences of buggy formula") - -# Replace all occurrences with correct formula -correct_formula = "$|V_{ud}|^2 + |V_{us}|^2 + |V_{ub}|^2 = 1$" -content = content.replace(buggy_formula, correct_formula) - -# Count occurrences after replacement -count_after = content.count(correct_formula) -print(f"Replaced {count_before} -> {count_after} occurrences") - -with open(tex_file, 'w') as f: - f.write(content) - -print(f"Lines modified: {len(content) - len(content.split(chr(10)))}") - -# Verify -print("\nVerification:") -if "1.47 \\times 10^{-4}" in content: - print(" ✓ BUG-1 fixed: 10^{-53} → 10^{-4}") -else: - print(" ✗ BUG-1 not found") - -# Check for buggy CKM formula (should NOT be found anymore) -if buggy_formula in content: - print(" ✗ BUG-2 still present - should be fixed") -else: - print(" ✓ BUG-2 fixed: all CKM unitarity formulas corrected") diff --git a/specs/claw/gates.t27 b/specs/claw/gates.t27 index 863d5c546e..9b04e3790f 100644 --- a/specs/claw/gates.t27 +++ b/specs/claw/gates.t27 @@ -23,7 +23,7 @@ ; it does not know is not a gate list (fail-closed applies to the inventory reader in T2). ; EVENTS are the `on:` trigger keys of each workflow, comma-joined, as measured 2026-10-12; they ; are recorded data for the report, not a class rule -- the class rule above is the law. -; Snapshot: 66 workflows on master, 2026-10-13. Census: 61 blocking + 3 soft + 2 reporting. +; Snapshot: 67 workflows on master, 2026-10-18 (pr-budget added, #10074). Census: 62 blocking + 3 soft + 2 reporting. ; phi^2 + 1/phi^2 = 3 | TRINITY module ClawGates; @@ -37,11 +37,11 @@ pub const G_UNKNOWN : u8 = 255; /// Total workflow files in .github/workflows/ this inventory claims to cover. /// T2 fails when the directory and this number disagree. -pub const COUNT : u32 = 66; +pub const COUNT : u32 = 67; /// Workflow names (file stems, no .yml), alphabetical. Row i of NAMES, CLASSES /// and EVENTS describes the same workflow -- the alignment is pinned by tests. -pub const NAMES : [66]str = [ +pub const NAMES : [67]str = [ "agent-runner-docker", "auto-merge-ready-prs", "bootstrap-tests", "build-paper", "build-vivado-image", "catalog-count-gate", "cli-tri", "conflict-markers", "conformance-integrity-gate", "coq-kernel", "coq-proofs", "corpus-ratchet", "damage-negatives", "deploy-api", @@ -49,7 +49,7 @@ pub const NAMES : [66]str = [ "fpga-build", "gate-topology", "gf-wide-conformance", "gf16-conformance", "harness-scratch", "issue-gate", "l1-traceability", "lean-proofs", "loop-tools-gate", "needs-boundary", "oracle-nightly", "orphan-modules", "own-language", "pack-index-consistency-gate", "phi-loop-ci", - "pr-dashboard", "pusher", "queen-doctor", "queen-export-push", "queen-feed-empty-bodies", + "pr-budget", "pr-dashboard", "pusher", "queen-doctor", "queen-export-push", "queen-feed-empty-bodies", "queen-feed-roadmap", "queen-feed-untested", "queen-publish", "queen-refile", "queen-watchdog", "release", "rings-rust", "sandbox-docker", "sbom", "schema-validation", "scorecard", "seal-coverage", "seal-staleness-warn", "secret-scan", "sign-release", @@ -63,13 +63,13 @@ pub const NAMES : [66]str = [ /// affected.t27 idiom); this table is the one hand-generated piece and the /// test len_tables_match_their_strings pins each row to NAMES[i].len, so a /// wrong number fails the suite rather than miscomparing. -pub const NAME_LENS : [66]usize = [ +pub const NAME_LENS : [67]usize = [ 19, 20, 15, 11, 18, 18, 7, 16, 26, 10, 10, 14, 16, 10, 19, 12, 18, 18, 18, 10, 13, 19, 16, 15, 10, 15, 11, 15, 14, 14, - 14, 12, 27, 11, 12, 6, + 14, 12, 27, 11, 9, 12, 6, 12, 17, 23, 18, 19, 13, 12, 14, 7, 10, 14, 4, 17, 9, 13, 19, 11, 12, @@ -79,7 +79,7 @@ pub const NAME_LENS : [66]usize = [ /// Class of row i, as literals so the array cannot silently depend on another /// const: 0 blocking, 1 soft, 2 reporting. The mapping is pinned right below. -pub const CLASSES : [66]u8 = [ +pub const CLASSES : [67]u8 = [ 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -87,7 +87,7 @@ pub const CLASSES : [66]u8 = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, - 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, @@ -97,7 +97,7 @@ pub const CLASSES : [66]u8 = [ ]; /// Trigger keys of row i, comma-joined, as measured 2026-10-12 (see header). -pub const EVENTS : [66]str = [ +pub const EVENTS : [67]str = [ "push,workflow_dispatch", "schedule,pull_request_target,workflow_dispatch", "push,pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "workflow_dispatch", "pull_request,workflow_dispatch", "pull_request,push", "pull_request,push,workflow_dispatch", "push,pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "push,workflow_dispatch", @@ -105,7 +105,7 @@ pub const EVENTS : [66]str = [ "pull_request,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "push,pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request_target,workflow_dispatch", "pull_request,workflow_dispatch", "pull_request,workflow_dispatch", "pull_request,push,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request", "push,pull_request,workflow_dispatch", "pull_request,workflow_dispatch", - "workflow_dispatch,pull_request", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", + "pull_request_target", "workflow_dispatch,pull_request", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "push,schedule,workflow_dispatch", "schedule,workflow_dispatch", "schedule,workflow_dispatch", "release", "push,pull_request,workflow_dispatch", "push,workflow_dispatch", "push,release,workflow_dispatch", "pull_request,push,workflow_dispatch", "branch_protection_rule,schedule,push,workflow_dispatch", "pull_request,push,workflow_dispatch", "pull_request,workflow_dispatch", "push,pull_request,workflow_dispatch", "release,workflow_dispatch", @@ -118,7 +118,7 @@ pub const EVENTS : [66]str = [ /// inventory. name_len is explicit: gen-c lowers str to const char* and /// carries no .len (the affected.t27 idiom, kept everywhere in this module). pub fn class_of(name: str, name_len: usize) -> u8 { - for i in 0..66 { + for i in 0..67 { if str_eq(NAMES[i], NAME_LENS[i], name, name_len) { return CLASSES[i]; } } return G_UNKNOWN; @@ -132,7 +132,7 @@ pub fn is_blocking(name: str, name_len: usize) -> bool { /// Trigger keys of one workflow name; "" when the name is not in the inventory. pub fn events_of(name: str, name_len: usize) -> str { - for i in 0..66 { + for i in 0..67 { if str_eq(NAMES[i], NAME_LENS[i], name, name_len) { return EVENTS[i]; } } return ""; @@ -141,7 +141,7 @@ pub fn events_of(name: str, name_len: usize) -> str { /// How many workflows carry a given class. pub fn count_class(c: u8) -> u32 { let mut n : u32 = 0; - for i in 0..66 { + for i in 0..67 { if CLASSES[i] == c { n += 1; } } return n; @@ -149,7 +149,7 @@ pub fn count_class(c: u8) -> u32 { /// Every row has a name and a recorded trigger set. pub fn every_row_filled() -> bool { - for i in 0..66 { + for i in 0..67 { if NAMES[i] == "" { return false; } if EVENTS[i] == "" { return false; } } @@ -171,8 +171,8 @@ pub fn str_eq(a: str, alen: usize, b: str, blen: usize) -> bool { } pub fn all_names_unique() -> bool { - for i in 0..66 { - for j in (i + 1)..66 { + for i in 0..67 { + for j in (i + 1)..67 { if str_eq(NAMES[i], NAME_LENS[i], NAMES[j], NAME_LENS[j]) { return false; } } } @@ -181,7 +181,7 @@ pub fn all_names_unique() -> bool { /// Every class literal in CLASSES is one of the three real classes. pub fn all_classes_known() -> bool { - for i in 0..66 { + for i in 0..67 { if CLASSES[i] != G_BLOCKING { if CLASSES[i] != G_SOFT { if CLASSES[i] != G_REPORTING { @@ -267,7 +267,7 @@ pub fn dir_has(dir_text : []const u8, dir_len : usize, name : str, name_len : us /// Whether one directory line names any inventory row -- the DIR-ONLY lookup. fn line_in_names(dir_text : []const u8, start : usize, len : usize) -> bool { - for i in 0..66 { + for i in 0..67 { if (line_eq_name(dir_text, start, len, NAMES[i], NAME_LENS[i])) { return true; } } return false; @@ -353,7 +353,7 @@ pub fn claw_status(dir_text : []const u8, dir_len : usize, out : []u8, out_cap : w = put_str(out, out_cap, w, AUDIT_HEAD, HEAD_LEN); w = put_str(out, out_cap, w, "\n", 1); // spec rows whose workflow file is gone - for i in 0..66 { + for i in 0..67 { if (dir_has(dir_text, dir_len, NAMES[i], NAME_LENS[i]) == false) { w = put_str(out, out_cap, w, LINE_PREFIX[P_SPEC_ONLY], PREFIX_LENS[P_SPEC_ONLY]); w = put_str(out, out_cap, w, NAMES[i], NAME_LENS[i]); @@ -410,7 +410,7 @@ pub fn claw_coverage_report(out : []u8, out_cap : usize) -> usize { w = put_str(out, out_cap, w, " blocking=", 10); var shown : usize = 0; var i : usize = 0; - while (i < 66) { + while (i < 67) { if (shown < 3) { if (CLASSES[i] == c) { if (shown == 0) { @@ -474,8 +474,8 @@ test critical_gates_present_and_blocking { assert is_blocking(c, 12) == true; } -test class_census_61_3_2 { - assert count_class(G_BLOCKING) == 61; +test class_census_62_3_2 { + assert count_class(G_BLOCKING) == 62; assert count_class(G_SOFT) == 3; assert count_class(G_REPORTING) == 2; } @@ -486,7 +486,7 @@ test rows_stay_aligned_name_class_events { // hand-counted literals (gen-c carries no .len); the tables are pinned in // gates_pins.t27. var want1 : str = "auto-merge-ready-prs"; - var want41 : str = "queen-publish"; + var want42 : str = "queen-publish"; var want29 : str = "oracle-nightly"; var qp : str = "queen-publish"; var ol : str = "own-language"; @@ -494,8 +494,8 @@ test rows_stay_aligned_name_class_events { var ev_ol : str = "pull_request"; assert NAMES[1] == want1; assert CLASSES[1] == G_REPORTING; - assert NAMES[41] == want41; - assert CLASSES[41] == G_REPORTING; + assert NAMES[42] == want42; + assert CLASSES[42] == G_REPORTING; assert NAMES[29] == want29; assert CLASSES[29] == G_SOFT; assert events_of(qp, 13) == ev_qp; @@ -523,7 +523,7 @@ test audit_clean_when_directory_matches_inventory { // a synthetic directory: every inventory name, one per line var dir : [8192]u8 = undefined; var w : usize = 0; - for i in 0..66 { + for i in 0..67 { w = put_str(&dir, 8192, w, NAMES[i], NAME_LENS[i]); w = put_str(&dir, 8192, w, "\n", 1); } @@ -539,7 +539,7 @@ test audit_spec_only_when_a_workflow_file_is_missing { // row 0 left out of the directory: its file is gone var dir : [8192]u8 = undefined; var w : usize = 0; - for i in 1..66 { + for i in 1..67 { w = put_str(&dir, 8192, w, NAMES[i], NAME_LENS[i]); w = put_str(&dir, 8192, w, "\n", 1); } @@ -554,7 +554,7 @@ test audit_dir_only_for_an_unknown_workflow { // the directory lists one workflow the inventory does not know var dir : [8192]u8 = undefined; var w : usize = 0; - for i in 0..66 { + for i in 0..67 { w = put_str(&dir, 8192, w, NAMES[i], NAME_LENS[i]); w = put_str(&dir, 8192, w, "\n", 1); } @@ -569,7 +569,7 @@ test audit_ignores_blank_lines_in_the_directory { // one must not turn cleanliness into drift: blank lines are skipped var dir : [8192]u8 = undefined; var w : usize = 0; - for i in 0..66 { + for i in 0..67 { w = put_str(&dir, 8192, w, NAMES[i], NAME_LENS[i]); w = put_str(&dir, 8192, w, "\n", 1); } @@ -591,11 +591,11 @@ test audit_prefixes_name_the_problem_kind { } test every_row_round_trips_through_the_name_lookup { // Before T8, 8 of 66 names ever went through class_of -- a class swap - // between two unexercised rows passed every test. This drives all 66 + // between two unexercised rows passed every test. This drives all 67 // rows through the lookup, and the merge verdict with them: blocking // iff the class is G_BLOCKING, for every row. var i : usize = 0; - while (i < 66) { + while (i < 67) { assert class_of(NAMES[i], NAME_LENS[i]) == CLASSES[i]; if (CLASSES[i] == G_BLOCKING) { assert is_blocking(NAMES[i], NAME_LENS[i]) == true; @@ -629,11 +629,11 @@ test coverage_report_carries_every_class { assert buf_has(&out, w, "G_BLOCKING", 10) == true; assert buf_has(&out, w, "G_SOFT", 6) == true; assert buf_has(&out, w, "G_REPORTING", 11) == true; - assert buf_has(&out, w, "n=61", 4) == true; + assert buf_has(&out, w, "n=62", 4) == true; assert buf_has(&out, w, "n=3", 3) == true; assert buf_has(&out, w, "n=2", 3) == true; assert buf_has(&out, w, "n=0", 3) == true; - assert buf_has(&out, w, "n=62", 4) == false; + assert buf_has(&out, w, "n=63", 4) == false; assert buf_has(&out, w, "G_UNKNOWN", 9) == true; } diff --git a/specs/claw/gates_pins.t27 b/specs/claw/gates_pins.t27 index 024114eb91..9295e05657 100644 --- a/specs/claw/gates_pins.t27 +++ b/specs/claw/gates_pins.t27 @@ -17,7 +17,7 @@ module ClawGatesPins; use claw::gates; test name_lens_pin_every_row_to_the_bytes { - for i in 0..66 { + for i in 0..67 { assert gates::NAME_LENS[i] == gates::NAMES[i].len; } } diff --git a/specs/policy/pr_budget.t27 b/specs/policy/pr_budget.t27 index 975d137ff5..e9e3e4388a 100644 --- a/specs/policy/pr_budget.t27 +++ b/specs/policy/pr_budget.t27 @@ -14,16 +14,21 @@ ; runs; as one batch it is about 48. ; -- ; Rule 2, budget. Each author may hold at most OPEN_CAP open non-draft PRs. Every author counts the -; same, the owner's account and the Queen's bees included: the cap is on the queue, not on a person. +; same, the owner's account included: the cap is on the queue, not on a person. One exception, the +; owner's decision of 2026-10-18 (#10074): a PR on a `queen-` branch of this repository is a Queen +; bee's, does not count, and is never held. The branch must live here, not in a fork, because a fork +; can name a branch anything. ; When the cap is full, the excess is held as a draft, and drafts are promoted in order as slots free. ; Nothing is closed and no work is refused; a held PR only waits. ; -- -; This card is the rule only. Wiring it into a gate that drafts the excess is a follow-up; until then -; nothing reads this file but its own tests. +; The gate (#10074) is .github/workflows/pr-budget.yml: plumbing around budget() below, lowered by +; `t27c gen-c` to gen/c/policy/pr_budget.c (L2). ; phi^2 + 1/phi^2 = 3 | TRINITY module PolicyPrBudget; +use policy::text_lines; + /// Open non-draft PRs one author may hold. The starting value; tune it here, nowhere else. pub const OPEN_CAP : u32 = 20; @@ -126,6 +131,162 @@ pub fn permille(part: u32, whole: u32) -> u32 { return part * 1000 / whole; } +; --- The gate (#10074) ------------------------------------------------------------------------------ +; .github/workflows/pr-budget.yml feeds budget() one row per line and carries out what it prints. A row +; is `S X N BRANCH`: S the state, X `1` when the head branch lives in a fork and `0` when it lives here, +; N the PR number in decimal, BRANCH the head branch. The first row is the PR the event is about: S is +; `O` when it asks to be ready (opened, reopened or ready_for_review, and not a draft) and `C` when a +; slot may have freed (closed, converted to a draft, opened as a draft). Every further row is another +; open PR of the same author, oldest first: `R` ready, `H` a draft this gate holds (label +; pr-budget-held), `D` a draft its author made, which is never promoted. A row that does not read is +; skipped. budget() prints `draft N` for an `O` PR the cap holds and `ready N` for each held draft +; promoted() frees, oldest first. No output closes a PR. + +pub const EXEMPT_BRANCH : str = "queen-"; +pub const EXEMPT_BRANCH_LEN : usize = 6; +pub const ACT_DRAFT : str = "draft "; +pub const ACT_READY : str = "ready "; +pub const ACT_LEN : usize = 6; + +/// Does buf[s..e] start with p[0..pn]? +pub fn has_prefix_at(buf: []const u8, s: usize, e: usize, p: []const u8, pn: usize) -> bool { + if (s + pn > e) { return false; } + var i : usize = 0; + while (i < pn) { + if (buf[s + i] != p[i]) { return false; } + i += 1; + } + return true; +} + +/// Is the branch buf[s..e] a Queen bee's? Only a `queen-` branch that lives in this repository. +pub fn is_exempt_at(buf: []const u8, s: usize, e: usize, fork: bool) -> bool { + if (fork) { return false; } + return has_prefix_at(buf, s, e, EXEMPT_BRANCH, EXEMPT_BRANCH_LEN); +} + +pub fn is_exempt(branch: []const u8, bn: usize, fork: bool) -> bool { + return is_exempt_at(branch, 0, bn, fork); +} + +pub fn is_digit(c: u8) -> bool { + return c >= '0' and c <= '9'; +} + +/// Where the digits of the row buf[s..e] stop. +pub fn digits_end(buf: []const u8, s: usize, e: usize) -> usize { + var i : usize = s + 4; + while (i < e and is_digit(buf[i])) { i += 1; } + return i; +} + +/// Does buf[s..e] read as `S X N BRANCH`, X `0` or `1`, N at most 9 digits, BRANCH not empty? +pub fn row_ok(buf: []const u8, s: usize, e: usize) -> bool { + if (e < s + 7) { return false; } + if (buf[s + 1] != ' ' or buf[s + 3] != ' ') { return false; } + if (buf[s + 2] != '0' and buf[s + 2] != '1') { return false; } + const d : usize = digits_end(buf, s, e); + if (d == s + 4 or d > s + 13) { return false; } + return d + 1 < e and buf[d] == ' '; +} + +/// The PR number of a row that reads. +pub fn row_number(buf: []const u8, s: usize, e: usize) -> u32 { + const d : usize = digits_end(buf, s, e); + var v : u32 = 0; + var i : usize = s + 4; + while (i < d) { + v = v * 10 + @as(u32, buf[i] - '0'); + i += 1; + } + return v; +} + +/// Is the PR of a row that reads exempt from the cap? +pub fn row_exempt(buf: []const u8, s: usize, e: usize) -> bool { + return is_exempt_at(buf, digits_end(buf, s, e) + 1, e, buf[s + 2] == '1'); +} + +/// Does the row buf[s..e] read, carry state st, and count against the cap? +pub fn row_counts(buf: []const u8, s: usize, e: usize, st: u8) -> bool { + if (row_ok(buf, s, e) == false) { return false; } + if (buf[s] != st) { return false; } + return row_exempt(buf, s, e) == false; +} + +/// How many rows after the first carry state st and count against the cap. +pub fn count_state(buf: []const u8, n: usize, st: u8) -> u32 { + var c : u32 = 0; + var s : usize = line_end(buf, 0, n) + 1; + while (s < n) { + const e : usize = line_end(buf, s, n); + if (row_counts(buf, s, e, st)) { c += 1; } + s = e + 1; + } + return c; +} + +/// Write one byte at w if it fits; the returned position keeps counting past the end. +pub fn put(out: []u8, w: usize, cap: usize, c: u8) -> usize { + if (w < cap) { out[w] = c; } + return w + 1; +} + +/// Write `draft N` or `ready N` and a newline. +pub fn put_act(out: []u8, w: usize, cap: usize, act: []const u8, v: u32) -> usize { + var p : usize = w; + var i : usize = 0; + while (i < ACT_LEN) { + p = put(out, p, cap, act[i]); + i += 1; + } + var d : u32 = 1; + while (v / d >= 10) { d = d * 10; } + while (d > 0) { + var c : u8 = '0'; + var r : u32 = (v / d) % 10; + while (r > 0) { + c += 1; + r -= 1; + } + p = put(out, p, cap, c); + d = d / 10; + } + return put(out, p, cap, '\n'); +} + +/// Read the rows buf[0..n], write the actions into out[0..cap], and return how many there are. +pub fn budget(buf: []const u8, n: usize, out: []u8, cap: usize) -> u32 { + return budget_with(buf, n, out, cap, OPEN_CAP); +} + +/// budget() with the cap as an input, so the tests can drive it with a small one. +pub fn budget_with(buf: []const u8, n: usize, out: []u8, cap: usize, limit: u32) -> u32 { + const e0 : usize = line_end(buf, 0, n); + if (row_ok(buf, 0, e0) == false) { return 0; } + const ready : u32 = count_state(buf, n, 'R'); + if (buf[0] == 'O') { + if (row_exempt(buf, 0, e0)) { return 0; } + if (admit(ready, limit) == READY) { return 0; } + if (put_act(out, 0, cap, ACT_DRAFT, row_number(buf, 0, e0)) > cap) { return 1; } + return 1; + } + if (buf[0] != 'C') { return 0; } + const k : u32 = promoted(ready, count_state(buf, n, 'H'), limit); + var acts : u32 = 0; + var w : usize = 0; + var s : usize = e0 + 1; + while (s < n and acts < k) { + const e : usize = line_end(buf, s, n); + if (row_counts(buf, s, e, 'H')) { + w = put_act(out, w, cap, ACT_READY, row_number(buf, s, e)); + acts += 1; + } + s = e + 1; + } + return acts; +} + test port_prs_are_named_by_title_or_branch { assert is_port_pr("spec(port): port specs/port/trios-railway/crates/trios-igla-race/src/race.t27", 77, "x", 1) == true; assert is_port_pr("chore: x", 8, "zcode-port-9776", 15) == true; @@ -190,9 +351,10 @@ test measured_2026_10_17_open_prs_against_the_cap { // read as if every open PR were ready: the share of each queue the cap would hold back assert held_as_draft(0, M_OPEN_FEDOROV, OPEN_CAP) == 108; assert held_as_draft(0, M_OPEN_GHASHTAG, OPEN_CAP) == 142; - // the owner's account is not exempt, and most of its queue is the Queen's bees + // most of the owner's queue is the Queen's bees, which are exempt (#10074): only 31 count assert M_OPEN_GHASHTAG_QUEEN * 2 > M_OPEN_GHASHTAG; assert M_OPEN_GHASHTAG - M_OPEN_GHASHTAG_QUEEN == 31; + assert held_as_draft(0, M_OPEN_GHASHTAG - M_OPEN_GHASHTAG_QUEEN, OPEN_CAP) == 11; } test measured_2026_10_17_rate_fills_the_cap_in_hours { @@ -214,6 +376,89 @@ test measured_2026_10_17_actions_runs { assert M_QUEUED_FEDOROV * 2 > M_QUEUED; } +test queen_branches_here_are_exempt_and_forks_are_not { + assert is_exempt("queen-10046", 11, false) == true; + assert is_exempt("queen-10046", 11, true) == false; + assert is_exempt("queen", 5, false) == false; + assert is_exempt("Queen-1", 7, false) == false; + assert is_exempt("zcode-port-9776", 15, false) == false; +} + +test a_row_reads_or_is_skipped { + assert row_ok("O 1 10070 port-x", 0, 16) == true; + assert row_number("O 1 10070 port-x", 0, 16) == 10070; + assert row_ok("O 2 1 a-1", 0, 9) == false; + assert row_ok("O 1 x port", 0, 10) == false; + assert row_ok("O 1 1 ", 0, 6) == false; + assert row_ok("O 1 1", 0, 5) == false; + assert row_ok("O 1 1234567890 a", 0, 16) == false; + assert row_ok("O 1 123456789 a", 0, 15) == true; + assert row_number("O 1 123456789 a", 0, 15) == 123456789; + assert row_exempt("R 0 1 queen-1", 0, 13) == true; + assert row_exempt("R 1 1 queen-1", 0, 13) == false; +} + +test put_act_writes_the_number_in_decimal { + var out : [32]u8 = undefined; + assert put_act(&out, 0, 32, ACT_DRAFT, 10070) == 12; + assert has_prefix(&out, 12, "draft 10070\n", 12) == true; + assert put_act(&out, 0, 32, ACT_READY, 0) == 8; + assert has_prefix(&out, 8, "ready 0\n", 8) == true; + assert put_act(&out, 0, 32, ACT_READY, 999999999) == 16; + assert has_prefix(&out, 16, "ready 999999999\n", 16) == true; + // a short buffer is never written past; the position still counts + var tiny : [4]u8 = undefined; + assert put_act(&tiny, 0, 4, ACT_DRAFT, 7) == 8; +} + +test the_pr_over_the_cap_is_drafted { + var out : [64]u8 = undefined; + assert budget_with("O 1 10070 port-x\nR 1 10001 a-1\nR 1 10002 a-2\n", 45, &out, 64, 2) == 1; + assert has_prefix(&out, 12, "draft 10070\n", 12) == true; + // one slot left: admitted, nothing printed + assert budget_with("O 1 10070 port-x\nR 1 10001 a-1\n", 31, &out, 64, 2) == 0; +} + +test the_cap_of_20_holds_the_21st_and_admits_the_20th { + var out : [64]u8 = undefined; + const twenty : []const u8 = "O 1 99 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\nR 1 1 b\n"; + assert budget(twenty, 169, &out, 64) == 1; + assert has_prefix(&out, 9, "draft 99\n", 9) == true; + // the same rows less the last one: 19 ready, so the 20th is admitted + assert budget(twenty, 161, &out, 64) == 0; +} + +test bees_are_never_drafted_and_never_counted { + var out : [64]u8 = undefined; + // a queen- branch here, over the cap: still admitted + assert budget_with("O 0 10070 queen-10070\nR 0 1 x-1\nR 0 2 x-2\n", 42, &out, 64, 2) == 0; + // a fork's queen- branch is not a bee + assert budget_with("O 1 10070 queen-1\nR 1 1 x-1\nR 1 2 x-2\n", 38, &out, 64, 2) == 1; + // the author's open bee PRs do not fill the cap: one counted ready PR of 2 + assert budget_with("O 0 10070 fix-a\nR 0 1 queen-1\nR 0 2 queen-2\nR 0 3 b-3\n", 54, &out, 64, 2) == 0; +} + +test a_freed_slot_promotes_the_oldest_held_drafts { + var out : [64]u8 = undefined; + // 1 ready, 3 held, cap 3: the 2 oldest held are promoted; the author's own draft 6 is not touched + assert budget_with("C 1 9 port-9\nR 1 1 a-1\nH 1 5 a-5\nD 1 6 a-6\nH 1 7 a-7\nH 1 8 a-8\n", 63, &out, 64, 3) == 2; + assert has_prefix(&out, 16, "ready 5\nready 7\n", 16) == true; + // no free slot: nothing + assert budget_with("C 1 9 port-9\nR 1 1 a-1\nH 1 5 a-5\nD 1 6 a-6\nH 1 7 a-7\nH 1 8 a-8\n", 63, &out, 64, 1) == 0; +} + +test rows_that_do_not_read_are_skipped { + var out : [64]u8 = undefined; + // a first row that does not read: nothing at all + assert budget_with("O 1 x port\nR 1 1 a\nR 1 2 b\n", 27, &out, 64, 1) == 0; + assert budget_with("X 1 1 a-1\nR 1 2 b-2\n", 20, &out, 64, 1) == 0; + // three broken rows after the first are not counted: 1 ready of 2 admits, of 1 holds + assert budget_with("O 1 7 a-7\nR 1 1 a-1\nR 2 2 b-2\nR 1 3\nR 1 x c-4\n", 46, &out, 64, 2) == 0; + assert budget_with("O 1 7 a-7\nR 1 1 a-1\nR 2 2 b-2\nR 1 3\nR 1 x c-4\n", 46, &out, 64, 1) == 1; + // a last row without its newline still reads + assert budget_with("O 1 99 b", 8, &out, 64, 0) == 1; +} + invariant credit_does_not_depend_on_pr_count { // wave_credit takes no PR count, so any batching of the same files earns the same assert wave_credit(1) == TRI_PER_SPEC;