From be5c0ed4d5fc33561d7d28e73e1653f536e43946 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Sat, 10 Oct 2026 12:57:14 +0200 Subject: [PATCH 01/10] feat(laya-train): LT-0 reads + rule checker for APR-LAYA-TRAIN-001 (Refs #4002) LT-0 report and receipt under evidence/laya-train/lt-0/. The rule checker scripts/check_laya_train_rules.sh prints one line per cmd rule (H1 H2 H4 H5 H10 H11 H12 H15); its --self-test plants 21 cases and runs in workspace-test via fragment 795. Co-Authored-By: Claude Opus 5.5 --- .../795-laya-train-rules-self-test.cmd | 2 + evidence/laya-train/lt-0/receipt.json | 19 + evidence/laya-train/lt-0/report.md | 169 +++++++++ scripts/check_laya_train_rules.sh | 332 ++++++++++++++++++ 4 files changed, 522 insertions(+) create mode 100644 ci/explicit-test-commands.d/795-laya-train-rules-self-test.cmd create mode 100644 evidence/laya-train/lt-0/receipt.json create mode 100644 evidence/laya-train/lt-0/report.md create mode 100755 scripts/check_laya_train_rules.sh diff --git a/ci/explicit-test-commands.d/795-laya-train-rules-self-test.cmd b/ci/explicit-test-commands.d/795-laya-train-rules-self-test.cmd new file mode 100644 index 0000000000..97384dec4a --- /dev/null +++ b/ci/explicit-test-commands.d/795-laya-train-rules-self-test.cmd @@ -0,0 +1,2 @@ +# APR-LAYA-TRAIN-001 LT-0(g): the rule checker plants each cmd rule of §9 and must name it RED. +bash scripts/check_laya_train_rules.sh --self-test diff --git a/evidence/laya-train/lt-0/receipt.json b/evidence/laya-train/lt-0/receipt.json new file mode 100644 index 0000000000..098e825ef8 --- /dev/null +++ b/evidence/laya-train/lt-0/receipt.json @@ -0,0 +1,19 @@ +{ + "row": "LT-0", + "spec": "APR-LAYA-TRAIN-001", + "state": "measured", + "main_sha": "e39f91a65e", + "pr4941_head": "e7730a3a4c", + "pr4634_head": "dad7f6a5e0", + "host": "noah-Lambda-Vector", + "measured_utc": "2026-10-10T11:00Z", + "report": "evidence/laya-train/lt-0/report.md", + "enforcing": ["scripts/check_laya_train_rules.sh --self-test"], + "ci_fragment": "ci/explicit-test-commands.d/795-laya-train-rules-self-test.cmd", + "command": "bash scripts/check_laya_train_rules.sh --base origin/main --head HEAD", + "self_test_cases": 21, + "py_added": 0, + "ops_absent_core": 9, + "carry_on_main": false, + "questions": ["Q1 surface", "Q2 CPU pool host", "Q3 E8 coupling", "Q4 dataset licence"] +} diff --git a/evidence/laya-train/lt-0/report.md b/evidence/laya-train/lt-0/report.md new file mode 100644 index 0000000000..146f897974 --- /dev/null +++ b/evidence/laya-train/lt-0/report.md @@ -0,0 +1,169 @@ +# LT-0: first reads (APR-LAYA-TRAIN-001) + +Measured 2026-10-10, 10:50Z to 11:00Z, on `noah-Lambda-Vector` (48 threads, 125 GiB), by the +worker `laya-train` (claude-opus-5-5). Nothing was built or run except the rule checker's +self-test. `main` = `e39f91a65e` (committed 2026-10-10T08:04:52Z). PR heads were read from +`refs/pull/4941/head` = `e7730a3a4c` and `refs/pull/4634/head` = `dad7f6a5e0`, read-only. + +## (a) Baseline facts re-measured on `main` + +| # | Spec said | Measured | Command | +|---|---|---|---| +| G1 | `6e7ea0133` | `e39f91a65e`, 2026-10-10T08:04:52Z. `main` moved; the spec's facts below still hold | `git rev-parse origin/main`; `git log -1 --format=%cI` | +| G2 | #4941 is a draft from a fork, one commit `e7730a3` | still OPEN, draft, head `e7730a3a4c`, not merged | `gh pr view 4941 --json state,isDraft,headRefOid,mergedAt` | +| G8 | gate contract 5.0.0, 13 equations, 6 binding rows, the seven unbound | holds on `e7730a3`: 5.0.0, 13 equations; `contracts/aprender/binding.yaml` has 6 rows (gate_pass, ece_top_label, split_disjointness, seed_selection_median, shift_probe_reported, synthetic_not_deployable), none of the seven. 54 lines of the gate contract and 6 of the parity contract name the `just laya-train-selftest` recipe or a `.py` file | `git show refs/laya/pr4941:` | +| G10 | 18 `deny_unknown_fields` in `pack.rs`; `torch_version` required; `device_is_cpu` check | holds: 18; `torch_version` is a required `String` with no value check (`pack.rs:340`); `verify.rs:1805` | same | +| G11 | doors public, take a policy | holds: `verify.rs:3397`, `:3429`; `VerifyPolicy` all 18 fields `pub`, no `Default`, one constructor `from_contract_views(&GateContractView, &ParityContractView)` (`:378`); `pack::load_checkpoint_for_scoring` `pack.rs:802`, and `scorer_from_parts` `:819` | same | +| G16, G17 | core and train engines; absent ops | holds, with detail in (c) | file reads on `main` | +| G18 | three encoders on `main`, modernbert absent | holds: `models/bert/` (7 files), `setfit/encoder.rs`, `aprender-train/src/transformer/encoder.rs`; `models/modernbert` absent | `git ls-tree` | +| G19 | `aprender-train` depends on `aprender-core` | holds: `crates/aprender-train/Cargo.toml:85`, a path dependency | file | +| G20 | `safetensors_is_read_only_by_pack` | holds: `lib.rs:341-398` scans every `.rs` under `src/` for `safetensors::` and `SafeTensors`; only `src/pack.rs` and `src/test_support.rs` allowed; `tests/` and `examples/` are not scanned | file | +| G22 | 170 `.py` | 170 | `git ls-tree -r --name-only origin/main \| grep -c '\.py$'` | +| G27 | no `benchmarks/tweeteval-stance/`, no CLI command | holds: 0 paths, 0 matches | `git ls-tree`; `grep -c tweet-eval-stance contracts/apr-cli-commands-v1.yaml` | + +## (b) The carry of #4941 + +Not merged; no carry PR exists (`gh pr list --search 4941 --state all` returns #4941 only). +`verify.rs`, `pack.rs` and `models/modernbert/` are identical on `e7730a3` and `dad7f6a`. +The two heads differ in the contracts only: `laya-finetune-gate-v1.yaml` gains one line +(`- tweet-eval-stance-benchmark-v1`, a reference) on `dad7f6a`, and `laya-parity-v1.yaml` +differs by 8 lines. The seven stay in the contract on both heads. Rows LT-3 on wait (§5). + +## (c) Operation table + +P+B present with backward · P−B present without backward · A absent. `core/` = +`crates/aprender-core/src/`, `train/` = `crates/aprender-train/src/`. Each backward was +found where its grad struct is attached to the output. + +| Op | core | train | +|---|---|---| +| matmul 2D | P+B `core/autograd/ops/activation.rs:274`, `MatmulBackward` `gradient.rs:110` | P+B `train/autograd/ops/matmul.rs:453` | +| batched matmul | P+B, 4D only, `pub(crate)`: `nn/transformer/positional_encoding.rs:326`, `BatchedMatmul4dBackward` `grad_fn.rs:1553` | A | +| bias add (broadcast) | P+B `broadcast_add` `activation.rs:376` (`nn::functional::linear`'s `broadcast_add_1d` drops the gradient; use `nn::Linear`) | P−B `train/transformer/attention.rs:16`, `feedforward.rs:217` | +| elementwise mul | P+B `autograd/ops/mod.rs:86` | P+B `train/autograd/ops/basic.rs:54` | +| layer_norm (bias / no bias) | P+B `nn/functional.rs:369`, `LayerNormBackward` `grad_fn.rs:858` | P−B batched `train/transformer/norm.rs:247`; the autograd one normalises a whole 1-D tensor | +| softmax | P+B 2D `activation.rs:238`; last-dim N-D `positional_encoding.rs:499` (`pub(super)`) | P+B 1-D `train/autograd/ops/activations.rs:174` | +| log / exp / sqrt / div | P+B `ops/mod.rs:227`, `:205`, `:267`, `:117` | A (scalar `scale` only) | +| gelu exact | P+B `activation.rs:183` | A | +| gelu tanh | P+B `activation.rs:121` | P+B `activations.rs:52` | +| relu | P+B `activation.rs:9` | P+B `activations.rs:10` | +| embedding gather | P+B `autograd/ops/embedding.rs:42`, scatter-add `grad_fn.rs:1450` | P−B `train/transformer/embedding.rs:63` | +| slice / narrow | **A** | A | +| chunk / split (fused `Wqkv`) | **A** | A | +| concat | **A** | A | +| clamp | **A** | A | +| max | **A** | A | +| additive mask, padding | P+B `autograd/ops/masking.rs:47` + `add_mask` `positional_encoding.rs:480` | A (causal hard-coded) | +| additive mask, local window | **A**: a constant `[S,S]` mask can be built and combined with padding | A | +| MHA self, batched, masked | P+B `nn/transformer/mod.rs:390` (chain of backwards through `grad_fn.rs:1496-1647`) | P+B unbatched, no padding mask, `train/transformer/attention.rs:471` | +| rotate-half RoPE, two thetas | **P−B, wrong layout**: `nn/transformer/attention_helpers.rs:270` is interleaved and returns a tensor with no grad | P+B rotate-half, private, one theta: `train/transformer/attention.rs:79`, `RopeBackward` `:148` | +| seeded dropout | P+B `nn/dropout/mod.rs:77` | A | +| reshape / transpose | P+B `activation.rs:428`, `:328`, `positional_encoding.rs:221`, `:548` | P+B (flat tensors) | +| marker gather | **A**; `embedding_gather` over `[B·S,H]` with positions as ids is a P+B workaround | P−B CLS only, `train/transformer/encoder.rs:173` | +| sum / mean | P+B full reductions `ops/mod.rs:320`, `:342` | P+B sum only | +| cross_entropy | P+B `nn/loss.rs:259` | P+B, stops at the logits, `train/train/loss/cross_entropy.rs:38` | +| spherical score norm | P+B `autograd/ops/normalize.rs:60`, `similarity.rs:59` | A | +| grad clip, global norm | **A on tensors**; `metrics/grad_norm.rs:72` works on `&mut [f64]` | present `train/optim/clip.rs:23` | +| AdamW, two groups | **A**: one rate `nn/optim/mod.rs:391`; two instances are the workaround. `Optimizer::step` for AdamW does not update parameters; `step_with_params` (`rm_sprop.rs:87`) does | A, one rate `train/optim/adamw.rs:16` | +| AdamW decay on a parameter with no gradient | **skips** it: `rm_sprop.rs:46-48`. G5 decays every parameter, so LT-6 must give each parameter a gradient (zeros) or change the step | skips: `adamw.rs:120` | +| cosine schedule with floor | present `nn/scheduler/mod.rs:188`, `with_min_lr` `:215`; not clamped past `t_max` | present, clamps | +| F16 save | present `serialization/safetensors.rs:382` (round-to-nearest-even `:284`) | A (scalar converter only) | + +Counts: core absent 9 (slice, chunk, concat, clamp, max, local-window mask, marker gather, +tensor grad clipping, AdamW groups); core present-without 1 (RoPE, wrong layout). Train +absent 16, present-without 5. Core's `TransformerEncoderLayer` is pre-norm with a tanh GELU +and biased projections (`nn/transformer/mod.rs:589-613`, `:243-246`), as F6 says. +Provisional reading, for LT-1 to decide: core needs fewer additions; train's encoder parts +(norm, feed-forward, embedding, bias add) have no backward. + +## (d) The judge table + +Every item the trainer calls is public. All modules are `pub` (`lib.rs:45-50`). + +| Item the trainer calls | Where (on `e7730a3`) | +|---|---| +| `verify::pack_for_serving`, `verify::verify_path` | `verify.rs:3397`, `:3429` | +| `verify::VerifyPolicy::from_contract_views` + overwrite of the pub field `base` with `BasePins` (tiny pins); the views are `pub` + `Deserialize`, the caller parses YAML | `verify.rs:96-141`, `:146-163`, `:378`; the crate's own tests do this, `verify/tests.rs:58-77` | +| `pack::load_checkpoint_for_scoring`, `pack::scorer_from_parts` | `pack.rs:802`, `:819` | +| probabilities: `DecisionMethod::classify` on `Laya`; `Laya::classify_for_task`; raw logits `Laya::forward_row` | `lib.rs:159`, `:182`; `laya/mod.rs:478`, `:410` | +| `laya::temperature::{bucket_key, clamp_temperature, softmax_t, TEMP_MIN, TEMP_MAX}` | `laya/temperature.rs:12-64` | +| data reader `verify::read_data_dir` → `DataDir { task, train, eval, shift }` | `verify.rs:1254`, `:1196` | +| `verify::normalize_text`, `normalized_sha256` | `verify.rs:1638`, `:1645` | +| `artifact::*` manifest types (Serialize + Deserialize) | `artifact.rs:129-251` | + +Not public, and not needed (no stop 3): +- No function derives the calibration slice; `check_split` only validates it. The trainer + writes its own split with its own declared generator (F7). +- `pack::Recipe` and `pack::GateReport` derive `Deserialize` only. The trainer writes the + JSON with its own serde structs and proves each file by reading it back through the + judge's door, so a key drift is refused by `deny_unknown_fields`. +- The tiny fixture path is `pub(crate)` under `cfg(test)`; the trainer names + `crates/aprender-decide/tests/fixtures/laya_tiny` as a path. The fixture itself is + `synthetic-fixture` (its `recipe.json` has epochs 0, seed 20260925), so both doors refuse + it as `SyntheticNotDeployable`, as the spec's LT-7 tiny acceptance already expects; a + `production` tiny run must be written by the trainer. + +Appendix C re-read, differences from the spec only: +- `t_applied` is compared bit for bit with the **clamped** `temperature_by_options[bucket]` + (`artifact.rs:1617-1677`, `temperature.rs:44-53`); the report also needs + `t_applied == clamp(t_fitted, 0.5, 5.0)` and `clamp_hit` consistent (`verify.rs:1804-1846`). +- Of the files "copied byte for byte from the base", only `tokenizer.json` is compared + (`pack.rs:728-732`, `verify.rs:1569-1575`). The checkpoint's `encoder/config.json` is not + compared with the base; the base dir's four files are pinned (`verify.rs:1565-1568`). +- Probability rows: `text_sha256` is the hash of the **raw** eval text, not the normalised + one; values finite, in [0,1], sum to 1 within 1e-5 (`verify.rs:1852-1957`). +- `seeds.label` is checked only under `seed_selection` (`verify.rs:2630`). + +Which of the seven the judge re-checks: + +| Equation | Judge re-checks? | +|---|---| +| calibration_fit_bounded | partly: bound and clamp relations (`verify.rs:1804-1846`); it cannot re-fit `t_fitted` | +| declared_seed_ships | partly: `recipe.seed == 13` (`:1405`), `seeds.declared == recipe.seed` (`:2601`) | +| eval_set_in_distribution | no | +| recipe_before_scores | no (only `recipe_id == sha256(recipe.json)`, `pack.rs:678`) | +| device_recorded | yes, `verify.rs:1805-1815` | +| f16_reload_scoring | no directly; implied by the re-score of the packed bytes (`:1984`) | +| early_stopping_train_side | no (only the recipe block, `:1426-1444`) | + +So LT-7 feeds the judge the plants of `device_recorded`, and the partial ones of +`calibration_fit_bounded` and `declared_seed_ships`. + +## (e) How a command runs on a CPU pool host today + +Nothing fits. No document defines "CPU pool host" or names one. Every dispatchable +self-hosted workflow runs on a `clean-room` runner, `intel,perf-solo` (the clean-room-16 +box) or a GPU runner; no make target dispatches remote work (the nearest pattern is +`nightly-train-run`, a local `systemctl --user` unit). Per the spec, LT-1's PR adds one +workflow that starts on `workflow_dispatch` only, and its runner label is question Q2. + +## (f) Licence of the dataset files + +TweetEval on Hugging Face (`cardiffnlp/tweet_eval`): licence tag "unknown"; stance subsets +"Undefined"; Twitter terms apply. The original SemEval-2016 Task 6 data (NRC Canada, +saifmohammad.com/WebPages/StanceDataset.htm): "available free for research purposes", +cite the paper; commercial use by request; "Do not redistribute". Reading: research and +benchmark use by this open-source trainer is allowed; tweet text, labels and derived splits +never enter git (H12 already says so); prefer the NRC download over the third-party mirror. +This is not stop 8 by the worker's reading; it is Q4 for the cop. + +## (g) The rule checker + +`scripts/check_laya_train_rules.sh` prints one line per *cmd* rule of §9 (H1, H2, H4, H5, +H10, H11, H12, H15) for `...`, each with its anti-vacuity arm, and exits 1 on a +RED. `--self-test` plants 21 cases in a throwaway repository (each rule's mutant must be +named RED, each clean case PASS) and runs in `workspace-test` through +`ci/explicit-test-commands.d/795-laya-train-rules-self-test.cmd`. Two readings of the spec, +stated so they can be overruled: +- H4: a head that adds LT-1's own receipt may touch `crates/`, so LT-1's PR can carry its + proxy measurement code. +- H5: the chosen engine is read from `"engine"` in LT-1's receipt on the base. + +## (h) Questions to the cop + +- Q1 surface: default, the example binary stays and no `apr` verb is added. +- Q2 host for LT-1 and LT-10: which runner label or host is "a CPU pool host"? Default: none + is guessed; LT-1 is prepared on `laya-train/wip` and its PR adds one `workflow_dispatch` + workflow whose `runs-on` the cop names. +- Q3 E8's recipe-schema row: default, no coupling. +- Q4 dataset licence: default, research use, NRC source, no redistribution, proceed. diff --git a/scripts/check_laya_train_rules.sh b/scripts/check_laya_train_rules.sh new file mode 100755 index 0000000000..c170a7a6c1 --- /dev/null +++ b/scripts/check_laya_train_rules.sh @@ -0,0 +1,332 @@ +#!/usr/bin/env bash +# check_laya_train_rules.sh - the rule checker of APR-LAYA-TRAIN-001 (§9, row LT-0 g). +# +# Prints one line per hard rule of the spec that is a command (H1, H2, H4, H5, +# H10, H11, H12, H15), for the diff `...`. Each line carries the +# rule's verdict and its anti-vacuity arm, the fact that shows the rule looked +# at something. The cop runs it on every PR of this spec before the merge queue +# is armed and posts the output on the PR. +# +# scripts/check_laya_train_rules.sh [--repo DIR] [--base REF] [--head REF] +# scripts/check_laya_train_rules.sh --self-test +# +# Exit 0 = every rule PASS (or n/a). Exit 1 = at least one RED. Exit 2 = usage. +# +# --self-test builds a throwaway repository under mktemp, plants each rule's +# mutant on its own branch and requires the checker to name it, and requires a +# clean branch to pass. It never runs git in this checkout. +# +# Needles that would match this file's own text are assembled from pieces, so +# a PR that adds this file does not trip H1 or H15 on itself. + +set -euo pipefail + +PY_WORD="pyth""on" +PIP_WORD="pi""p " +UV_WORD="uv"" run" +DIRTY_FLAG="--allow""-dirty" +SSH_WORD="ss""h " +PUBLISH_RE='(cargo|make)[[:space:]]+'"publi""sh" + +LT1_RECEIPT="evidence/laya-train/lt-1/receipt.json" +LT9_DECL="evidence/laya-train/lt-9/declaration.json" +LT10_DIR="evidence/laya-train/lt-10" +NEW_CRATE="crates/aprender-decide-train" +SIZE_CAP=1048576 + +REPO="" +BASE="origin/main" +HEAD_REF="HEAD" +RED=0 + +usage() { + printf 'usage: %s [--repo DIR] [--base REF] [--head REF] | --self-test\n' "$0" >&2 + exit 2 +} + +g() { git -C "$REPO" "$@"; } + +line() { + # line + printf '%-4s %-4s | %s | arm: %s\n' "$1" "$2" "$3" "$4" + if [ "$2" = "RED" ]; then RED=1; fi +} + +changed_names() { g diff --name-only "$BASE...$HEAD_REF"; } +added_names() { g diff --name-only --diff-filter=A "$BASE...$HEAD_REF"; } + +# Paths whose added lines count as "a Makefile, a justfile, a script or a workflow". +runner_paths() { + changed_names | awk '/(^|\/)Makefile[^\/]*$|\.mk$|(^|\/)justfile$|\.sh$|\.bash$|^scripts\/|^\.github\/workflows\//' +} + +# Added lines (without the leading '+') of runner paths, as "path:text". +added_runner_lines() { + local p + while IFS= read -r p; do + [ -n "$p" ] || continue + g diff -U0 "$BASE...$HEAD_REF" -- "$p" \ + | awk -v p="$p" '/^\+\+\+/ {next} /^\+/ {print p ":" substr($0, 2)}' + done < <(runner_paths) +} + +rule_h1() { + local files lines hits + files=$(added_names | awk '/\.py$|(^|\/)pyproject\.toml$|(^|\/)uv\.lock$/') + lines=$(added_runner_lines | awk -v a="$PY_WORD" -v b="$UV_WORD" -v c="$PIP_WORD" \ + 'index($0, a) || index($0, b) || index($0, c)') + hits=$(printf '%s\n%s\n' "$files" "$lines" | awk 'NF' | head -n 5 | paste -sd ';' -) + if [ -n "$hits" ]; then + line H1 RED "Python added: $hits" "files and runner lines scanned" + else + line H1 PASS "no .py/pyproject/uv.lock added, no runner line invokes it" \ + "$(runner_paths | awk 'NF' | wc -l) runner paths, $(added_names | awk 'NF' | wc -l) added files scanned" + fi +} + +rule_h2() { + local hits + hits=$(changed_names | awk '/^crates\/aprender-decide\/|^crates\/aprender-core\/src\/models\/modernbert\/|^scripts\/laya_train\/numeric_cases\.json$|\/fixtures\/(modernbert_tiny|laya_tiny)\//' \ + | head -n 5 | paste -sd ';' -) + if [ -n "$hits" ]; then + line H2 RED "judge path changed: $hits" "name-only diff" + else + line H2 PASS "no judge path changed" "$(changed_names | awk 'NF' | wc -l) changed paths scanned" + fi +} + +rule_h4() { + local on_base=absent on_head=absent touches + g cat-file -e "$BASE:$LT1_RECEIPT" 2>/dev/null && on_base=present + g cat-file -e "$HEAD_REF:$LT1_RECEIPT" 2>/dev/null && on_head=present + touches=$(changed_names | awk '/^crates\//' | wc -l) + local arm="$LT1_RECEIPT base=$on_base head=$on_head; crates/ paths=$touches" + if [ "$touches" -gt 0 ] && [ "$on_base" = absent ] && [ "$on_head" = absent ]; then + line H4 RED "touches crates/ before LT-1's receipt" "$arm" + else + line H4 PASS "measure-first holds" "$arm" + fi +} + +rule_h5() { + local engine other files imports_ok second_tensor + if ! g cat-file -e "$HEAD_REF:$NEW_CRATE/Cargo.toml" 2>/dev/null; then + line H5 n/a "$NEW_CRATE absent on head" "crate looked up at $HEAD_REF" + return + fi + engine=$(g show "$BASE:$LT1_RECEIPT" 2>/dev/null \ + | awk -F'"' '/"engine"[[:space:]]*:/ {print $4; exit}') + case "$engine" in + core) other='aprender_train::autograd|entrenar::autograd|^[[:space:]]*aprender-train[[:space:]]*=|package[[:space:]]*=[[:space:]]*"aprender-train"' ;; + train) other='aprender::autograd|aprender_core::autograd' ;; + *) + line H5 RED "no chosen engine in $BASE:$LT1_RECEIPT" "engine='${engine:-none}'" + return ;; + esac + files=$(g ls-tree -r --name-only "$HEAD_REF" -- "$NEW_CRATE" | awk '/\.rs$|Cargo\.toml$/') + local hits="" n_ok=0 f body + while IFS= read -r f; do + [ -n "$f" ] || continue + body=$(g show "$HEAD_REF:$f") + if printf '%s\n' "$body" | grep -Eq "$other"; then hits="$hits $f"; fi + if [ "$engine" = core ]; then + printf '%s\n' "$body" | grep -Eq 'aprender::autograd' && n_ok=$((n_ok + 1)) + else + printf '%s\n' "$body" | grep -Eq 'aprender_train::autograd|entrenar::autograd' && n_ok=$((n_ok + 1)) + fi + done <<<"$files" + second_tensor=$(g diff -U0 "$BASE...$HEAD_REF" -- '*.rs' | awk ' + /^\+\+\+ / {f=$2; next} + /^\+[[:space:]]*(pub(\([a-z]+\))?[[:space:]]+)?struct[[:space:]]+Tensor([^A-Za-z0-9_]|$)/ {t[f]=1} + /^\+.*fn[[:space:]]+backward[[:space:]]*\(/ {b[f]=1} + END {for (k in t) if (k in b) print k}' | paste -sd ';' -) + if [ -n "$hits" ] || [ -n "$second_tensor" ]; then + line H5 RED "other engine named:${hits:- none}; second Tensor with backward: ${second_tensor:-none}" \ + "engine=$engine" + else + line H5 PASS "only the chosen engine" "engine=$engine; its import found in $n_ok file(s)" + fi +} + +utc_commit_time() { + # utc_commit_time : commit time of the commit that added + TZ=UTC g log --diff-filter=A --date=format-local:%Y-%m-%dT%H:%M:%SZ --format='%cd %h' \ + "$1" -- "$2" | tail -n 1 +} + +rule_h10() { + local decl outcomes n bad="" f started + if ! g cat-file -e "$BASE:$LT9_DECL" 2>/dev/null; then + if g cat-file -e "$HEAD_REF:$LT10_DIR" 2>/dev/null \ + && g ls-tree --name-only "$HEAD_REF" "$LT10_DIR/" | awk '/outcome/' | grep -q .; then + line H10 RED "outcome present but no declaration on $BASE" "$LT9_DECL absent on base" + else + line H10 n/a "no declaration on $BASE" "$LT9_DECL absent on base" + fi + return + fi + decl=$(utc_commit_time "$BASE" "$LT9_DECL") + outcomes=$(g ls-tree -r --name-only "$HEAD_REF" -- "$LT10_DIR" | awk '/outcome[^\/]*\.json$/') + n=$(printf '%s\n' "$outcomes" | awk 'NF' | wc -l) + while IFS= read -r f; do + [ -n "$f" ] || continue + started=$(g show "$HEAD_REF:$f" | awk -F'"' '/"run_started_utc"[[:space:]]*:/ {print $4; exit}') + if [ -z "$started" ] || [[ "$started" < "${decl%% *}" ]]; then bad="$bad $f(${started:-no run_started_utc})"; fi + done <<<"$outcomes" + if [ "$n" -gt 1 ] || [ -n "$bad" ]; then + line H10 RED "outcomes=$n; before declaration:${bad:- none}" "declaration merged $decl" + else + line H10 PASS "outcomes=$n, none before the declaration" "declaration merged $decl" + fi +} + +rule_h11() { + local cli pub new_crates n_new wf bad_wf="" p + cli=$(changed_names | awk '$0 == "contracts/apr-cli-commands-v1.yaml"') + pub=$(changed_names | awk '/(^|\/)Cargo\.toml$/' | while IFS= read -r p; do + g diff -U0 "$BASE...$HEAD_REF" -- "$p" | awk -v p="$p" '/^[+-][[:space:]]*publish[[:space:]]*=/ {print p; exit}' + done | paste -sd ';' -) + new_crates=$(added_names | awk '/^crates\/[^\/]+\/Cargo\.toml$/' | paste -sd ';' -) + n_new=$(printf '%s' "$new_crates" | awk -F';' 'NF {print NF; exit} END {if (!NR) print 0}') + wf=$(changed_names | awk '/^\.github\/workflows\/.*\.ya?ml$/') + while IFS= read -r p; do + [ -n "$p" ] || continue + g cat-file -e "$HEAD_REF:$p" 2>/dev/null || continue + if g show "$HEAD_REF:$p" | awk ' + /^on:[[:space:]]*[^[:space:]]/ { if ($0 !~ /^on:[[:space:]]*\[?[[:space:]]*workflow_dispatch[[:space:]]*\]?[[:space:]]*$/) bad=1; next } + /^on:/ {inon=1; next} + inon && /^[^[:space:]#]/ {inon=0} + inon && /^ [A-Za-z_]+:?/ { k=$1; sub(/:$/, "", k); if (k != "workflow_dispatch") bad=1 } + END {exit (bad ? 0 : 1)}'; then + bad_wf="$bad_wf $p" + fi + done <<<"$wf" + if [ -n "$cli" ] || [ -n "$pub" ] || [ "$n_new" -gt 1 ] || [ -n "$bad_wf" ]; then + line H11 RED "cli-contract:${cli:-no} publish:${pub:-no} new crates:$n_new workflows:${bad_wf:- ok}" \ + "new crates: ${new_crates:-none}" + else + line H11 PASS "no public surface added" "new crates: ${new_crates:-none}" + fi +} + +rule_h12() { + local f s max=0 maxf="none" over="" + while IFS= read -r f; do + [ -n "$f" ] || continue + s=$(g cat-file -s "$HEAD_REF:$f" 2>/dev/null || echo 0) + if [ "$s" -gt "$max" ]; then max=$s; maxf=$f; fi + if [ "$s" -gt "$SIZE_CAP" ]; then over="$over $f($s)"; fi + done < <(added_names) + if [ -n "$over" ]; then + line H12 RED "added file above $SIZE_CAP bytes:$over" "largest added: $maxf $max" + else + line H12 PASS "no added file above $SIZE_CAP bytes" "largest added: $maxf $max" + fi +} + +rule_h15() { + local hits + hits=$(added_runner_lines | awk -v a="$DIRTY_FLAG" -v b="$SSH_WORD" -v re="$PUBLISH_RE" \ + 'index($0, a) || index($0, b) || $0 ~ re' | head -n 5 | paste -sd ';' -) + if [ -n "$hits" ]; then + line H15 RED "forbidden line added: $hits" "runner lines scanned" + else + line H15 PASS "no dirty-publish, publish or ssh line added" \ + "$(added_runner_lines | wc -l) added runner lines scanned" + fi +} + +run_rules() { + g rev-parse -q --verify "$BASE^{commit}" >/dev/null || { echo "base $BASE not found" >&2; exit 2; } + g rev-parse -q --verify "$HEAD_REF^{commit}" >/dev/null || { echo "head $HEAD_REF not found" >&2; exit 2; } + printf 'laya-train rules | base %s | head %s\n' \ + "$(g rev-parse --short "$BASE")" "$(g rev-parse --short "$HEAD_REF")" + rule_h1; rule_h2; rule_h4; rule_h5; rule_h10; rule_h11; rule_h12; rule_h15 + if [ "$RED" -eq 0 ]; then echo "RESULT PASS"; else echo "RESULT RED"; fi + return "$RED" +} + +# ---------------------------------------------------------------- self-test + +st_commit() { git -C "$ST" -c core.hooksPath=/dev/null -c user.name=t -c user.email=t@t commit -qm "$1"; } + +st_case() { + # st_case : setup runs in $ST on a fresh branch. + local name=$1 expect=$2 rule=$3 out rc=0 + shift 3 + git -C "$ST" checkout -q -B "$name" base + (cd "$ST" && "$@") + git -C "$ST" add -A + st_commit "$name" + out=$("$SELF" --repo "$ST" --base base --head "$name") || rc=$? + local got + got=$(printf '%s\n' "$out" | awk -v r="$rule" '$1 == r {print $2; exit}') + if [ "$got" = "$expect" ] && { [ "$expect" = RED ] && [ "$rc" -eq 1 ] || [ "$expect" != RED ]; }; then + printf 'ok %-22s %s %s\n' "$name" "$rule" "$got" + else + printf 'FAIL %-22s %s want %s got %s rc=%s\n%s\n' "$name" "$rule" "$expect" "${got:-none}" "$rc" "$out" + ST_FAIL=1 + fi +} + +self_test() { + SELF=$(cd "$(dirname "$0")" && pwd)/$(basename "$0") + ST=$(mktemp -d) + case "$ST" in /tmp/*|"${TMPDIR:-/tmp}"/*) ;; *) echo "mktemp gave $ST" >&2; exit 2 ;; esac + trap 'rm -rf -- "${ST:?}"' EXIT + export GIT_CEILING_DIRECTORIES + GIT_CEILING_DIRECTORIES=$(dirname "$ST") + ST_FAIL=0 + git -C "$ST" init -q + mkdir -p "$ST/crates/aprender-decide/src" "$ST/scripts" "$ST/.github/workflows" + echo 'fn verify() {}' >"$ST/crates/aprender-decide/src/verify.rs" + echo 'echo hi' >"$ST/scripts/a.sh" + git -C "$ST" add -A + st_commit base + git -C "$ST" branch -q -M base + + st_case clean PASS H1 sh -c 'echo "# docs" > notes.md' + st_case plant-py-file RED H1 sh -c 'echo "x = 1" > x.py' + st_case plant-py-line RED H1 sh -c "echo '${PY_WORD}3 run.py' >> scripts/a.sh" + st_case plant-judge-edit RED H2 sh -c 'echo "// x" >> crates/aprender-decide/src/verify.rs' + st_case plant-crates-early RED H4 sh -c 'mkdir -p crates/x && echo "[package]" > crates/x/Cargo.toml' + st_case lt1-own-receipt PASS H4 sh -c "mkdir -p crates/x evidence/laya-train/lt-1 && echo '[package]' > crates/x/Cargo.toml && echo '{\"engine\": \"core\"}' > $LT1_RECEIPT" + st_case plant-cli-contract RED H11 sh -c 'mkdir -p contracts && echo "x: 1" > contracts/apr-cli-commands-v1.yaml' + st_case plant-two-crates RED H11 sh -c 'mkdir -p crates/a crates/b && echo "[package]" > crates/a/Cargo.toml && echo "[package]" > crates/b/Cargo.toml' + st_case plant-push-trigger RED H11 sh -c 'mkdir -p .github/workflows && printf "on:\n push:\n workflow_dispatch:\njobs: {}\n" > .github/workflows/w.yml' + st_case dispatch-only PASS H11 sh -c 'mkdir -p .github/workflows && printf "on:\n workflow_dispatch:\njobs: {}\n" > .github/workflows/w.yml' + st_case plant-big-file RED H12 sh -c 'head -c 1048577 /dev/zero > big.bin' + st_case plant-dirty RED H15 sh -c "echo 'cargo package ${DIRTY_FLAG}' >> scripts/a.sh" + st_case plant-ssh RED H15 sh -c "echo '${SSH_WORD}host true' >> scripts/a.sh" + + # H5 and H10 need LT-1's receipt and LT-9's declaration on the base. + git -C "$ST" checkout -q base + mkdir -p "$ST/evidence/laya-train/lt-1" "$ST/evidence/laya-train/lt-9" + echo '{"engine": "core"}' >"$ST/$LT1_RECEIPT" + echo '{"seeds": [13, 17, 23]}' >"$ST/$LT9_DECL" + git -C "$ST" add -A + st_commit "receipts" + local nc="$NEW_CRATE" + st_case crate-core-ok PASS H5 sh -c "mkdir -p $nc/src && echo '[package]' > $nc/Cargo.toml && echo 'use aprender::autograd::Tensor;' > $nc/src/lib.rs" + st_case plant-other-engine RED H5 sh -c "mkdir -p $nc/src && echo '[package]' > $nc/Cargo.toml && echo 'use aprender_train::autograd::Tensor;' > $nc/src/lib.rs" + st_case plant-second-tensor RED H5 sh -c "mkdir -p $nc/src && echo '[package]' > $nc/Cargo.toml && printf 'use aprender::autograd::Tensor as T;\npub struct Tensor;\nimpl Tensor { fn backward(&self) {} }\n' > $nc/src/lib.rs" + st_case outcome-after PASS H10 sh -c "mkdir -p $LT10_DIR && echo '{\"run_started_utc\": \"2999-01-01T00:00:00Z\"}' > $LT10_DIR/outcome.json" + st_case plant-outcome-before RED H10 sh -c "mkdir -p $LT10_DIR && echo '{\"run_started_utc\": \"2000-01-01T00:00:00Z\"}' > $LT10_DIR/outcome.json" + st_case plant-two-outcomes RED H10 sh -c "mkdir -p $LT10_DIR && echo '{\"run_started_utc\": \"2999-01-01T00:00:00Z\"}' > $LT10_DIR/outcome.json && cp $LT10_DIR/outcome.json $LT10_DIR/outcome-2.json" + + if [ "$ST_FAIL" -eq 0 ]; then echo "SELF-TEST PASS"; else echo "SELF-TEST FAIL"; exit 1; fi +} + +# ---------------------------------------------------------------- main + +while [ $# -gt 0 ]; do + case "$1" in + --self-test) self_test; exit 0 ;; + --repo) [ $# -ge 2 ] || usage; REPO=$2; shift 2 ;; + --base) [ $# -ge 2 ] || usage; BASE=$2; shift 2 ;; + --head) [ $# -ge 2 ] || usage; HEAD_REF=$2; shift 2 ;; + *) usage ;; + esac +done +if [ -z "$REPO" ]; then REPO=$(cd "$(dirname "$0")/.." && pwd); fi +run_rules From 8665beb5a89d71b2cb1df5cb130311b30777f773 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Sat, 10 Oct 2026 13:23:30 +0200 Subject: [PATCH 02/10] feat(laya-train): H4 exempts exactly the LT-1 probe path (Q5 ruling), 3 self-test cases (Refs #4002) Co-Authored-By: Claude Opus 5.5 --- evidence/laya-train/lt-0/receipt.json | 2 +- evidence/laya-train/lt-0/report.md | 7 ++++--- scripts/check_laya_train_rules.sh | 12 ++++++++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/evidence/laya-train/lt-0/receipt.json b/evidence/laya-train/lt-0/receipt.json index 098e825ef8..85492636a9 100644 --- a/evidence/laya-train/lt-0/receipt.json +++ b/evidence/laya-train/lt-0/receipt.json @@ -11,7 +11,7 @@ "enforcing": ["scripts/check_laya_train_rules.sh --self-test"], "ci_fragment": "ci/explicit-test-commands.d/795-laya-train-rules-self-test.cmd", "command": "bash scripts/check_laya_train_rules.sh --base origin/main --head HEAD", - "self_test_cases": 21, + "self_test_cases": 24, "py_added": 0, "ops_absent_core": 9, "carry_on_main": false, diff --git a/evidence/laya-train/lt-0/report.md b/evidence/laya-train/lt-0/report.md index 146f897974..23291d0881 100644 --- a/evidence/laya-train/lt-0/report.md +++ b/evidence/laya-train/lt-0/report.md @@ -151,12 +151,13 @@ This is not stop 8 by the worker's reading; it is Q4 for the cop. `scripts/check_laya_train_rules.sh` prints one line per *cmd* rule of §9 (H1, H2, H4, H5, H10, H11, H12, H15) for `...`, each with its anti-vacuity arm, and exits 1 on a -RED. `--self-test` plants 21 cases in a throwaway repository (each rule's mutant must be +RED. `--self-test` plants 24 cases in a throwaway repository (each rule's mutant must be named RED, each clean case PASS) and runs in `workspace-test` through `ci/explicit-test-commands.d/795-laya-train-rules-self-test.cmd`. Two readings of the spec, stated so they can be overruled: -- H4: a head that adds LT-1's own receipt may touch `crates/`, so LT-1's PR can carry its - proxy measurement code. +- H4: a head that adds LT-1's own receipt may touch `crates/`. One path is exempt by the Q5 + ruling (handoff/quorum-a3-laya-q5-lt1-order.md): `crates/aprender-train/examples/laya_engine_probe.rs`, + the proxy probe, which must merge before its receipt can exist; the arm prints the exempt count. - H5: the chosen engine is read from `"engine"` in LT-1's receipt on the base. ## (h) Questions to the cop diff --git a/scripts/check_laya_train_rules.sh b/scripts/check_laya_train_rules.sh index c170a7a6c1..d1f41e0189 100755 --- a/scripts/check_laya_train_rules.sh +++ b/scripts/check_laya_train_rules.sh @@ -29,6 +29,9 @@ SSH_WORD="ss""h " PUBLISH_RE='(cargo|make)[[:space:]]+'"publi""sh" LT1_RECEIPT="evidence/laya-train/lt-1/receipt.json" +# The ONE path H4 exempts (Q5 ruling, handoff/quorum-a3-laya-q5-lt1-order.md): the LT-1 +# proxy-layer probe is measurement, not model code, and must merge before its receipt. +LT1_PROBE="crates/aprender-train/examples/laya_engine_probe.rs" LT9_DECL="evidence/laya-train/lt-9/declaration.json" LT10_DIR="evidence/laya-train/lt-10" NEW_CRATE="crates/aprender-decide-train" @@ -99,8 +102,10 @@ rule_h4() { local on_base=absent on_head=absent touches g cat-file -e "$BASE:$LT1_RECEIPT" 2>/dev/null && on_base=present g cat-file -e "$HEAD_REF:$LT1_RECEIPT" 2>/dev/null && on_head=present - touches=$(changed_names | awk '/^crates\//' | wc -l) - local arm="$LT1_RECEIPT base=$on_base head=$on_head; crates/ paths=$touches" + local exempt + touches=$(changed_names | awk -v p="$LT1_PROBE" '/^crates\// && $0 != p' | wc -l) + exempt=$(changed_names | awk -v p="$LT1_PROBE" '$0 == p' | wc -l) + local arm="$LT1_RECEIPT base=$on_base head=$on_head; crates/ paths=$touches; exempt $LT1_PROBE=$exempt" if [ "$touches" -gt 0 ] && [ "$on_base" = absent ] && [ "$on_head" = absent ]; then line H4 RED "touches crates/ before LT-1's receipt" "$arm" else @@ -291,6 +296,9 @@ self_test() { st_case plant-judge-edit RED H2 sh -c 'echo "// x" >> crates/aprender-decide/src/verify.rs' st_case plant-crates-early RED H4 sh -c 'mkdir -p crates/x && echo "[package]" > crates/x/Cargo.toml' st_case lt1-own-receipt PASS H4 sh -c "mkdir -p crates/x evidence/laya-train/lt-1 && echo '[package]' > crates/x/Cargo.toml && echo '{\"engine\": \"core\"}' > $LT1_RECEIPT" + st_case lt1-probe-only PASS H4 sh -c "mkdir -p crates/aprender-train/examples && echo 'fn main() {}' > $LT1_PROBE" + st_case plant-probe-plus-crate RED H4 sh -c "mkdir -p crates/aprender-train/examples crates/x && echo 'fn main() {}' > $LT1_PROBE && echo '[package]' > crates/x/Cargo.toml" + st_case plant-probe-lookalike RED H4 sh -c "mkdir -p crates/aprender-train/examples && echo 'fn main() {}' > crates/aprender-train/examples/laya_engine_probe2.rs" st_case plant-cli-contract RED H11 sh -c 'mkdir -p contracts && echo "x: 1" > contracts/apr-cli-commands-v1.yaml' st_case plant-two-crates RED H11 sh -c 'mkdir -p crates/a crates/b && echo "[package]" > crates/a/Cargo.toml && echo "[package]" > crates/b/Cargo.toml' st_case plant-push-trigger RED H11 sh -c 'mkdir -p .github/workflows && printf "on:\n push:\n workflow_dispatch:\njobs: {}\n" > .github/workflows/w.yml' From d8522ea8ef09d61740c3b87c54b648a3ad6a2807 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Sat, 10 Oct 2026 13:25:00 +0200 Subject: [PATCH 03/10] fix(laya-train): H15 matches ssh in command position only; the checker no longer flags its own text (Refs #4002) Co-Authored-By: Claude Opus 5.5 --- evidence/laya-train/lt-0/receipt.json | 2 +- evidence/laya-train/lt-0/report.md | 2 +- scripts/check_laya_train_rules.sh | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/evidence/laya-train/lt-0/receipt.json b/evidence/laya-train/lt-0/receipt.json index 85492636a9..693567fe24 100644 --- a/evidence/laya-train/lt-0/receipt.json +++ b/evidence/laya-train/lt-0/receipt.json @@ -11,7 +11,7 @@ "enforcing": ["scripts/check_laya_train_rules.sh --self-test"], "ci_fragment": "ci/explicit-test-commands.d/795-laya-train-rules-self-test.cmd", "command": "bash scripts/check_laya_train_rules.sh --base origin/main --head HEAD", - "self_test_cases": 24, + "self_test_cases": 26, "py_added": 0, "ops_absent_core": 9, "carry_on_main": false, diff --git a/evidence/laya-train/lt-0/report.md b/evidence/laya-train/lt-0/report.md index 23291d0881..d1b21084fc 100644 --- a/evidence/laya-train/lt-0/report.md +++ b/evidence/laya-train/lt-0/report.md @@ -151,7 +151,7 @@ This is not stop 8 by the worker's reading; it is Q4 for the cop. `scripts/check_laya_train_rules.sh` prints one line per *cmd* rule of §9 (H1, H2, H4, H5, H10, H11, H12, H15) for `...`, each with its anti-vacuity arm, and exits 1 on a -RED. `--self-test` plants 24 cases in a throwaway repository (each rule's mutant must be +RED. `--self-test` plants 26 cases in a throwaway repository (each rule's mutant must be named RED, each clean case PASS) and runs in `workspace-test` through `ci/explicit-test-commands.d/795-laya-train-rules-self-test.cmd`. Two readings of the spec, stated so they can be overruled: diff --git a/scripts/check_laya_train_rules.sh b/scripts/check_laya_train_rules.sh index d1f41e0189..359ca8e042 100755 --- a/scripts/check_laya_train_rules.sh +++ b/scripts/check_laya_train_rules.sh @@ -26,6 +26,9 @@ PIP_WORD="pi""p " UV_WORD="uv"" run" DIRTY_FLAG="--allow""-dirty" SSH_WORD="ss""h " +# ssh only in command position (line start, or after ; & | ( or a backtick, optionally +# behind sudo/exec/command/time), so prose that names it is not a hit. +SSH_RE='(^|[;&|(`])[[:space:]]*((sudo|exec|command|time)[[:space:]]+)?'"ss""h"'[[:space:]]' PUBLISH_RE='(cargo|make)[[:space:]]+'"publi""sh" LT1_RECEIPT="evidence/laya-train/lt-1/receipt.json" @@ -231,8 +234,8 @@ rule_h12() { rule_h15() { local hits - hits=$(added_runner_lines | awk -v a="$DIRTY_FLAG" -v b="$SSH_WORD" -v re="$PUBLISH_RE" \ - 'index($0, a) || index($0, b) || $0 ~ re' | head -n 5 | paste -sd ';' -) + hits=$(added_runner_lines | awk -v a="$DIRTY_FLAG" -v s="$SSH_RE" -v re="$PUBLISH_RE" \ + '{ body = $0; sub(/^[^:]*:/, "", body) } index(body, a) || body ~ s || body ~ re' | head -n 5 | paste -sd ';' -) if [ -n "$hits" ]; then line H15 RED "forbidden line added: $hits" "runner lines scanned" else @@ -306,6 +309,8 @@ self_test() { st_case plant-big-file RED H12 sh -c 'head -c 1048577 /dev/zero > big.bin' st_case plant-dirty RED H15 sh -c "echo 'cargo package ${DIRTY_FLAG}' >> scripts/a.sh" st_case plant-ssh RED H15 sh -c "echo '${SSH_WORD}host true' >> scripts/a.sh" + st_case plant-ssh-chained RED H15 sh -c "echo 'true; sudo ${SSH_WORD}host true' >> scripts/a.sh" + st_case ssh-in-prose PASS H15 sh -c "echo '# no ${SSH_WORD}line is added here' >> scripts/a.sh" # H5 and H10 need LT-1's receipt and LT-9's declaration on the base. git -C "$ST" checkout -q base From bb57ca45e737c25e748357f05b178d3c11b45ef4 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Sat, 10 Oct 2026 13:28:25 +0200 Subject: [PATCH 04/10] fix(laya-train): H15 restores the plain ssh substring needle, skips only the checker by path; 12 planted forms (C314, Refs #4002) Co-Authored-By: Claude Opus 5.5 --- evidence/laya-train/lt-0/receipt.json | 2 +- evidence/laya-train/lt-0/report.md | 5 ++++- scripts/check_laya_train_rules.sh | 27 +++++++++++++++++++-------- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/evidence/laya-train/lt-0/receipt.json b/evidence/laya-train/lt-0/receipt.json index 693567fe24..14f89f1371 100644 --- a/evidence/laya-train/lt-0/receipt.json +++ b/evidence/laya-train/lt-0/receipt.json @@ -11,7 +11,7 @@ "enforcing": ["scripts/check_laya_train_rules.sh --self-test"], "ci_fragment": "ci/explicit-test-commands.d/795-laya-train-rules-self-test.cmd", "command": "bash scripts/check_laya_train_rules.sh --base origin/main --head HEAD", - "self_test_cases": 26, + "self_test_cases": 35, "py_added": 0, "ops_absent_core": 9, "carry_on_main": false, diff --git a/evidence/laya-train/lt-0/report.md b/evidence/laya-train/lt-0/report.md index d1b21084fc..bf8b1d4b32 100644 --- a/evidence/laya-train/lt-0/report.md +++ b/evidence/laya-train/lt-0/report.md @@ -151,13 +151,16 @@ This is not stop 8 by the worker's reading; it is Q4 for the cop. `scripts/check_laya_train_rules.sh` prints one line per *cmd* rule of §9 (H1, H2, H4, H5, H10, H11, H12, H15) for `...`, each with its anti-vacuity arm, and exits 1 on a -RED. `--self-test` plants 26 cases in a throwaway repository (each rule's mutant must be +RED. `--self-test` plants 35 cases in a throwaway repository (each rule's mutant must be named RED, each clean case PASS) and runs in `workspace-test` through `ci/explicit-test-commands.d/795-laya-train-rules-self-test.cmd`. Two readings of the spec, stated so they can be overruled: - H4: a head that adds LT-1's own receipt may touch `crates/`. One path is exempt by the Q5 ruling (handoff/quorum-a3-laya-q5-lt1-order.md): `crates/aprender-train/examples/laya_engine_probe.rs`, the proxy probe, which must merge before its receipt can exist; the arm prints the exempt count. +- H15: the needles are plain substrings, so every form of an ssh call is a hit. Exactly one + path is skipped (C314, handoff/c314-a3-laya-lt0-lt1a.md): `scripts/check_laya_train_rules.sh`, + which must name the needles to plant them; the arm prints the skipped path. - H5: the chosen engine is read from `"engine"` in LT-1's receipt on the base. ## (h) Questions to the cop diff --git a/scripts/check_laya_train_rules.sh b/scripts/check_laya_train_rules.sh index 359ca8e042..6af6fbcc62 100755 --- a/scripts/check_laya_train_rules.sh +++ b/scripts/check_laya_train_rules.sh @@ -26,10 +26,10 @@ PIP_WORD="pi""p " UV_WORD="uv"" run" DIRTY_FLAG="--allow""-dirty" SSH_WORD="ss""h " -# ssh only in command position (line start, or after ; & | ( or a backtick, optionally -# behind sudo/exec/command/time), so prose that names it is not a hit. -SSH_RE='(^|[;&|(`])[[:space:]]*((sudo|exec|command|time)[[:space:]]+)?'"ss""h"'[[:space:]]' PUBLISH_RE='(cargo|make)[[:space:]]+'"publi""sh" +# H15 skips exactly this file by path (C314, handoff/c314-a3-laya-lt0-lt1a.md): it must name +# the forbidden needles to plant them. The needle itself stays a plain substring. +H15_SELF="scripts/check_laya_train_rules.sh" LT1_RECEIPT="evidence/laya-train/lt-1/receipt.json" # The ONE path H4 exempts (Q5 ruling, handoff/quorum-a3-laya-q5-lt1-order.md): the LT-1 @@ -234,13 +234,13 @@ rule_h12() { rule_h15() { local hits - hits=$(added_runner_lines | awk -v a="$DIRTY_FLAG" -v s="$SSH_RE" -v re="$PUBLISH_RE" \ - '{ body = $0; sub(/^[^:]*:/, "", body) } index(body, a) || body ~ s || body ~ re' | head -n 5 | paste -sd ';' -) + hits=$(added_runner_lines | awk -v a="$DIRTY_FLAG" -v b="$SSH_WORD" -v re="$PUBLISH_RE" -v self="$H15_SELF:" \ + 'index($0, self) == 1 {next} { body = $0; sub(/^[^:]*:/, "", body) } index(body, a) || index(body, b) || body ~ re' | head -n 5 | paste -sd ';' -) if [ -n "$hits" ]; then - line H15 RED "forbidden line added: $hits" "runner lines scanned" + line H15 RED "forbidden line added: $hits" "runner lines scanned; path skipped: $H15_SELF" else line H15 PASS "no dirty-publish, publish or ssh line added" \ - "$(added_runner_lines | wc -l) added runner lines scanned" + "$(added_runner_lines | wc -l) added runner lines scanned; path skipped: $H15_SELF" fi } @@ -310,7 +310,18 @@ self_test() { st_case plant-dirty RED H15 sh -c "echo 'cargo package ${DIRTY_FLAG}' >> scripts/a.sh" st_case plant-ssh RED H15 sh -c "echo '${SSH_WORD}host true' >> scripts/a.sh" st_case plant-ssh-chained RED H15 sh -c "echo 'true; sudo ${SSH_WORD}host true' >> scripts/a.sh" - st_case ssh-in-prose PASS H15 sh -c "echo '# no ${SSH_WORD}line is added here' >> scripts/a.sh" + st_case plant-ssh-yaml-run RED H15 sh -c "mkdir -p .github/workflows && echo ' run: ${SSH_WORD}host true' > .github/workflows/w.yml" + st_case plant-ssh-then RED H15 sh -c "echo 'if true; then ${SSH_WORD}host true; fi' >> scripts/a.sh" + st_case plant-ssh-do RED H15 sh -c "echo 'for h in a; do ${SSH_WORD}\$h true; done' >> scripts/a.sh" + st_case plant-ssh-else RED H15 sh -c "echo 'if false; then :; else ${SSH_WORD}host true; fi' >> scripts/a.sh" + st_case plant-ssh-brace RED H15 sh -c "echo '{ ${SSH_WORD}host true; }' >> scripts/a.sh" + st_case plant-ssh-env RED H15 sh -c "echo 'env A=1 ${SSH_WORD}host true' >> scripts/a.sh" + st_case plant-ssh-xargs RED H15 sh -c "echo 'echo h | xargs -I{} ${SSH_WORD}{} true' >> scripts/a.sh" + st_case plant-ssh-nohup RED H15 sh -c "echo 'nohup ${SSH_WORD}host true &' >> scripts/a.sh" + st_case plant-ssh-timeout RED H15 sh -c "echo 'timeout 5 ${SSH_WORD}host true' >> scripts/a.sh" + st_case plant-ssh-abspath RED H15 sh -c "echo '/usr/bin/${SSH_WORD}host true' >> scripts/a.sh" + st_case h15-self-path PASS H15 sh -c "echo '# ${SSH_WORD}is a needle here' >> $H15_SELF" + st_case plant-ssh-self-lookalike RED H15 sh -c "echo '${SSH_WORD}host true' >> scripts/check_laya_train_rules2.sh" # H5 and H10 need LT-1's receipt and LT-9's declaration on the base. git -C "$ST" checkout -q base From 9194cb479109821d130188055657501416e29fad Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 12 Oct 2026 10:02:47 +0200 Subject: [PATCH 05/10] fix(laya-train): LT-0 report counts match its table: train absent 14, present-without 4; G8 gate lines 55 with the pattern stated (Refs #4002) Seat B (non-author review, haiku) recounted the (c) table's train column: 14 absent, 4 present-without, not 16/5. G8's gate-contract count re-derived on e7730a3 as 55 with grep -cE 'laya-train-selftest|\.py\b'; parity stays 6. Co-Authored-By: Claude Opus 5.5 --- evidence/laya-train/lt-0/report.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evidence/laya-train/lt-0/report.md b/evidence/laya-train/lt-0/report.md index bf8b1d4b32..1af07206d7 100644 --- a/evidence/laya-train/lt-0/report.md +++ b/evidence/laya-train/lt-0/report.md @@ -11,7 +11,7 @@ self-test. `main` = `e39f91a65e` (committed 2026-10-10T08:04:52Z). PR heads were |---|---|---|---| | G1 | `6e7ea0133` | `e39f91a65e`, 2026-10-10T08:04:52Z. `main` moved; the spec's facts below still hold | `git rev-parse origin/main`; `git log -1 --format=%cI` | | G2 | #4941 is a draft from a fork, one commit `e7730a3` | still OPEN, draft, head `e7730a3a4c`, not merged | `gh pr view 4941 --json state,isDraft,headRefOid,mergedAt` | -| G8 | gate contract 5.0.0, 13 equations, 6 binding rows, the seven unbound | holds on `e7730a3`: 5.0.0, 13 equations; `contracts/aprender/binding.yaml` has 6 rows (gate_pass, ece_top_label, split_disjointness, seed_selection_median, shift_probe_reported, synthetic_not_deployable), none of the seven. 54 lines of the gate contract and 6 of the parity contract name the `just laya-train-selftest` recipe or a `.py` file | `git show refs/laya/pr4941:` | +| G8 | gate contract 5.0.0, 13 equations, 6 binding rows, the seven unbound | holds on `e7730a3`: 5.0.0, 13 equations; `contracts/aprender/binding.yaml` has 6 rows (gate_pass, ece_top_label, split_disjointness, seed_selection_median, shift_probe_reported, synthetic_not_deployable), none of the seven. 55 lines of the gate contract and 6 of the parity contract name the `just laya-train-selftest` recipe or a `.py` file (`grep -cE 'laya-train-selftest\|\.py\b'`) | `git show refs/laya/pr4941:` | | G10 | 18 `deny_unknown_fields` in `pack.rs`; `torch_version` required; `device_is_cpu` check | holds: 18; `torch_version` is a required `String` with no value check (`pack.rs:340`); `verify.rs:1805` | same | | G11 | doors public, take a policy | holds: `verify.rs:3397`, `:3429`; `VerifyPolicy` all 18 fields `pub`, no `Default`, one constructor `from_contract_views(&GateContractView, &ParityContractView)` (`:378`); `pack::load_checkpoint_for_scoring` `pack.rs:802`, and `scorer_from_parts` `:819` | same | | G16, G17 | core and train engines; absent ops | holds, with detail in (c) | file reads on `main` | @@ -71,7 +71,7 @@ found where its grad struct is attached to the output. Counts: core absent 9 (slice, chunk, concat, clamp, max, local-window mask, marker gather, tensor grad clipping, AdamW groups); core present-without 1 (RoPE, wrong layout). Train -absent 16, present-without 5. Core's `TransformerEncoderLayer` is pre-norm with a tanh GELU +absent 14, present-without 4. Core's `TransformerEncoderLayer` is pre-norm with a tanh GELU and biased projections (`nn/transformer/mod.rs:589-613`, `:243-246`), as F6 says. Provisional reading, for LT-1 to decide: core needs fewer additions; train's encoder parts (norm, feed-forward, embedding, bias add) have no backward. From c7e1373aa0921e13ae40846f128bd418b7a86003 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 12 Oct 2026 10:03:52 +0200 Subject: [PATCH 06/10] fix(laya-train): LT-0 report quotes the G8 and G22 commands outside the table, unescaped, so they run as copied (Refs #4002) Seat B: the in-table `\|` made the quoted grep a literal-pipe match (0, not 55). Both pipelines now sit in plain notes below the table. Co-Authored-By: Claude Opus 5.5 --- evidence/laya-train/lt-0/report.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/evidence/laya-train/lt-0/report.md b/evidence/laya-train/lt-0/report.md index 1af07206d7..fc43a1324c 100644 --- a/evidence/laya-train/lt-0/report.md +++ b/evidence/laya-train/lt-0/report.md @@ -11,16 +11,20 @@ self-test. `main` = `e39f91a65e` (committed 2026-10-10T08:04:52Z). PR heads were |---|---|---|---| | G1 | `6e7ea0133` | `e39f91a65e`, 2026-10-10T08:04:52Z. `main` moved; the spec's facts below still hold | `git rev-parse origin/main`; `git log -1 --format=%cI` | | G2 | #4941 is a draft from a fork, one commit `e7730a3` | still OPEN, draft, head `e7730a3a4c`, not merged | `gh pr view 4941 --json state,isDraft,headRefOid,mergedAt` | -| G8 | gate contract 5.0.0, 13 equations, 6 binding rows, the seven unbound | holds on `e7730a3`: 5.0.0, 13 equations; `contracts/aprender/binding.yaml` has 6 rows (gate_pass, ece_top_label, split_disjointness, seed_selection_median, shift_probe_reported, synthetic_not_deployable), none of the seven. 55 lines of the gate contract and 6 of the parity contract name the `just laya-train-selftest` recipe or a `.py` file (`grep -cE 'laya-train-selftest\|\.py\b'`) | `git show refs/laya/pr4941:` | +| G8 | gate contract 5.0.0, 13 equations, 6 binding rows, the seven unbound | holds on `e7730a3`: 5.0.0, 13 equations; `contracts/aprender/binding.yaml` has 6 rows (gate_pass, ece_top_label, split_disjointness, seed_selection_median, shift_probe_reported, synthetic_not_deployable), none of the seven. 55 lines of the gate contract and 6 of the parity contract name the `just laya-train-selftest` recipe or a `.py` file (command in the G8 note below the table) | `git show refs/laya/pr4941:` | | G10 | 18 `deny_unknown_fields` in `pack.rs`; `torch_version` required; `device_is_cpu` check | holds: 18; `torch_version` is a required `String` with no value check (`pack.rs:340`); `verify.rs:1805` | same | | G11 | doors public, take a policy | holds: `verify.rs:3397`, `:3429`; `VerifyPolicy` all 18 fields `pub`, no `Default`, one constructor `from_contract_views(&GateContractView, &ParityContractView)` (`:378`); `pack::load_checkpoint_for_scoring` `pack.rs:802`, and `scorer_from_parts` `:819` | same | | G16, G17 | core and train engines; absent ops | holds, with detail in (c) | file reads on `main` | | G18 | three encoders on `main`, modernbert absent | holds: `models/bert/` (7 files), `setfit/encoder.rs`, `aprender-train/src/transformer/encoder.rs`; `models/modernbert` absent | `git ls-tree` | | G19 | `aprender-train` depends on `aprender-core` | holds: `crates/aprender-train/Cargo.toml:85`, a path dependency | file | | G20 | `safetensors_is_read_only_by_pack` | holds: `lib.rs:341-398` scans every `.rs` under `src/` for `safetensors::` and `SafeTensors`; only `src/pack.rs` and `src/test_support.rs` allowed; `tests/` and `examples/` are not scanned | file | -| G22 | 170 `.py` | 170 | `git ls-tree -r --name-only origin/main \| grep -c '\.py$'` | +| G22 | 170 `.py` | 170 | command in the G22 note below the table | | G27 | no `benchmarks/tweeteval-stance/`, no CLI command | holds: 0 paths, 0 matches | `git ls-tree`; `grep -c tweet-eval-stance contracts/apr-cli-commands-v1.yaml` | +G8 note: the 55 and 6 are `git show e7730a3a4c:contracts/laya-finetune-gate-v1.yaml | grep -cE 'laya-train-selftest|\.py\b'` and the same for `contracts/laya-parity-v1.yaml`. + +G22 note: the 170 is `git ls-tree -r --name-only origin/main | grep -c '\.py$'`. + ## (b) The carry of #4941 Not merged; no carry PR exists (`gh pr list --search 4941 --state all` returns #4941 only). From d2b6dc73ddc18e81e53c96db0aad234f040c816f Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 12 Oct 2026 10:07:52 +0200 Subject: [PATCH 07/10] fix(laya-train): LT-0 checker is dispatched by guard_tree --no-cargo and pipes nothing into grep -q (Refs #4002) check_guards_are_wired.sh read the checker as unwired: one fixture string held the literal build-tool word, so guard_tree filed it with the cargo guards no workflow runs. The word is now split; the checker runs no build tool. Four 'printf | grep -q' sites became here-strings or a -n test, which keeps check_no_pipe_into_grep_q.sh at its ceiling of 60. Self-test PASS. Co-Authored-By: Claude Opus 5.5 --- scripts/check_laya_train_rules.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/check_laya_train_rules.sh b/scripts/check_laya_train_rules.sh index 6af6fbcc62..cf66cd224d 100755 --- a/scripts/check_laya_train_rules.sh +++ b/scripts/check_laya_train_rules.sh @@ -25,6 +25,7 @@ PY_WORD="pyth""on" PIP_WORD="pi""p " UV_WORD="uv"" run" DIRTY_FLAG="--allow""-dirty" +BUILD_WORD="car""go" # split so guard_tree files this checker with the no-cargo guards: it runs no build tool SSH_WORD="ss""h " PUBLISH_RE='(cargo|make)[[:space:]]+'"publi""sh" # H15 skips exactly this file by path (C314, handoff/c314-a3-laya-lt0-lt1a.md): it must name @@ -136,11 +137,11 @@ rule_h5() { while IFS= read -r f; do [ -n "$f" ] || continue body=$(g show "$HEAD_REF:$f") - if printf '%s\n' "$body" | grep -Eq "$other"; then hits="$hits $f"; fi + if grep -Eq -- "$other" <<<"$body"; then hits="$hits $f"; fi if [ "$engine" = core ]; then - printf '%s\n' "$body" | grep -Eq 'aprender::autograd' && n_ok=$((n_ok + 1)) + grep -Eq 'aprender::autograd' <<<"$body" && n_ok=$((n_ok + 1)) else - printf '%s\n' "$body" | grep -Eq 'aprender_train::autograd|entrenar::autograd' && n_ok=$((n_ok + 1)) + grep -Eq 'aprender_train::autograd|entrenar::autograd' <<<"$body" && n_ok=$((n_ok + 1)) fi done <<<"$files" second_tensor=$(g diff -U0 "$BASE...$HEAD_REF" -- '*.rs' | awk ' @@ -166,7 +167,7 @@ rule_h10() { local decl outcomes n bad="" f started if ! g cat-file -e "$BASE:$LT9_DECL" 2>/dev/null; then if g cat-file -e "$HEAD_REF:$LT10_DIR" 2>/dev/null \ - && g ls-tree --name-only "$HEAD_REF" "$LT10_DIR/" | awk '/outcome/' | grep -q .; then + && [ -n "$(g ls-tree --name-only "$HEAD_REF" "$LT10_DIR/" | awk '/outcome/')" ]; then line H10 RED "outcome present but no declaration on $BASE" "$LT9_DECL absent on base" else line H10 n/a "no declaration on $BASE" "$LT9_DECL absent on base" @@ -307,7 +308,7 @@ self_test() { st_case plant-push-trigger RED H11 sh -c 'mkdir -p .github/workflows && printf "on:\n push:\n workflow_dispatch:\njobs: {}\n" > .github/workflows/w.yml' st_case dispatch-only PASS H11 sh -c 'mkdir -p .github/workflows && printf "on:\n workflow_dispatch:\njobs: {}\n" > .github/workflows/w.yml' st_case plant-big-file RED H12 sh -c 'head -c 1048577 /dev/zero > big.bin' - st_case plant-dirty RED H15 sh -c "echo 'cargo package ${DIRTY_FLAG}' >> scripts/a.sh" + st_case plant-dirty RED H15 sh -c "echo '${BUILD_WORD} package ${DIRTY_FLAG}' >> scripts/a.sh" st_case plant-ssh RED H15 sh -c "echo '${SSH_WORD}host true' >> scripts/a.sh" st_case plant-ssh-chained RED H15 sh -c "echo 'true; sudo ${SSH_WORD}host true' >> scripts/a.sh" st_case plant-ssh-yaml-run RED H15 sh -c "mkdir -p .github/workflows && echo ' run: ${SSH_WORD}host true' > .github/workflows/w.yml" From 7fa07ed1e6ac5aea49a994a5ddfe98359063f3d0 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 12 Oct 2026 10:30:58 +0200 Subject: [PATCH 08/10] docs(laya-train): LT-0 report, receipt and checker comments drop internal citations, the host name and runner labels (Refs #4002) Public-text hygiene on review. The comments and report now state each rule's exemption without citing where it was decided, and the receipt names the machine by its size. Co-Authored-By: Claude Opus 5.5 --- evidence/laya-train/lt-0/receipt.json | 2 +- evidence/laya-train/lt-0/report.md | 12 ++++++------ scripts/check_laya_train_rules.sh | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/evidence/laya-train/lt-0/receipt.json b/evidence/laya-train/lt-0/receipt.json index 14f89f1371..0a5f27228e 100644 --- a/evidence/laya-train/lt-0/receipt.json +++ b/evidence/laya-train/lt-0/receipt.json @@ -5,7 +5,7 @@ "main_sha": "e39f91a65e", "pr4941_head": "e7730a3a4c", "pr4634_head": "dad7f6a5e0", - "host": "noah-Lambda-Vector", + "host": "workstation, 48 threads, 125 GiB", "measured_utc": "2026-10-10T11:00Z", "report": "evidence/laya-train/lt-0/report.md", "enforcing": ["scripts/check_laya_train_rules.sh --self-test"], diff --git a/evidence/laya-train/lt-0/report.md b/evidence/laya-train/lt-0/report.md index fc43a1324c..b7fb303d46 100644 --- a/evidence/laya-train/lt-0/report.md +++ b/evidence/laya-train/lt-0/report.md @@ -1,6 +1,6 @@ # LT-0: first reads (APR-LAYA-TRAIN-001) -Measured 2026-10-10, 10:50Z to 11:00Z, on `noah-Lambda-Vector` (48 threads, 125 GiB), by the +Measured 2026-10-10, 10:50Z to 11:00Z, on a 48-thread, 125 GiB workstation, by the worker `laya-train` (claude-opus-5-5). Nothing was built or run except the rule checker's self-test. `main` = `e39f91a65e` (committed 2026-10-10T08:04:52Z). PR heads were read from `refs/pull/4941/head` = `e7730a3a4c` and `refs/pull/4634/head` = `dad7f6a5e0`, read-only. @@ -136,8 +136,8 @@ So LT-7 feeds the judge the plants of `device_recorded`, and the partial ones of ## (e) How a command runs on a CPU pool host today Nothing fits. No document defines "CPU pool host" or names one. Every dispatchable -self-hosted workflow runs on a `clean-room` runner, `intel,perf-solo` (the clean-room-16 -box) or a GPU runner; no make target dispatches remote work (the nearest pattern is +self-hosted workflow runs on a clean-room runner, a performance +runner or a GPU runner; no make target dispatches remote work (the nearest pattern is `nightly-train-run`, a local `systemctl --user` unit). Per the spec, LT-1's PR adds one workflow that starts on `workflow_dispatch` only, and its runner label is question Q2. @@ -159,11 +159,11 @@ RED. `--self-test` plants 35 cases in a throwaway repository (each rule's mutant named RED, each clean case PASS) and runs in `workspace-test` through `ci/explicit-test-commands.d/795-laya-train-rules-self-test.cmd`. Two readings of the spec, stated so they can be overruled: -- H4: a head that adds LT-1's own receipt may touch `crates/`. One path is exempt by the Q5 - ruling (handoff/quorum-a3-laya-q5-lt1-order.md): `crates/aprender-train/examples/laya_engine_probe.rs`, +- H4: a head that adds LT-1's own receipt may touch `crates/`. One path is exempt: + `crates/aprender-train/examples/laya_engine_probe.rs`, the proxy probe, which must merge before its receipt can exist; the arm prints the exempt count. - H15: the needles are plain substrings, so every form of an ssh call is a hit. Exactly one - path is skipped (C314, handoff/c314-a3-laya-lt0-lt1a.md): `scripts/check_laya_train_rules.sh`, + path is skipped: `scripts/check_laya_train_rules.sh`, which must name the needles to plant them; the arm prints the skipped path. - H5: the chosen engine is read from `"engine"` in LT-1's receipt on the base. diff --git a/scripts/check_laya_train_rules.sh b/scripts/check_laya_train_rules.sh index cf66cd224d..810cc853bb 100755 --- a/scripts/check_laya_train_rules.sh +++ b/scripts/check_laya_train_rules.sh @@ -28,12 +28,12 @@ DIRTY_FLAG="--allow""-dirty" BUILD_WORD="car""go" # split so guard_tree files this checker with the no-cargo guards: it runs no build tool SSH_WORD="ss""h " PUBLISH_RE='(cargo|make)[[:space:]]+'"publi""sh" -# H15 skips exactly this file by path (C314, handoff/c314-a3-laya-lt0-lt1a.md): it must name +# H15 skips exactly this file by path: it must name # the forbidden needles to plant them. The needle itself stays a plain substring. H15_SELF="scripts/check_laya_train_rules.sh" LT1_RECEIPT="evidence/laya-train/lt-1/receipt.json" -# The ONE path H4 exempts (Q5 ruling, handoff/quorum-a3-laya-q5-lt1-order.md): the LT-1 +# The ONE path H4 exempts: the LT-1 # proxy-layer probe is measurement, not model code, and must merge before its receipt. LT1_PROBE="crates/aprender-train/examples/laya_engine_probe.rs" LT9_DECL="evidence/laya-train/lt-9/declaration.json" From ca89c62ae7079a970a47d88c05ad3f2e3ffdad2c Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 12 Oct 2026 11:04:25 +0200 Subject: [PATCH 09/10] fix(laya-train): LT-0 checker reads a shallow pull-request merge by its first parent; bashrs SC1066 cleared (Refs #4002) guard_tree now runs the checker on the PR's CI clone, where HEAD is a depth-1 merge and the base is fetched alone, so base...head had no merge base and the run stopped. When the base is HEAD's first parent, read from the raw commit, the tree diff base..head is the PR's diff; any other unrelated base still stops with exit 2. Two self-test cases build that clone (a planted .py is RED; main's own .py is not charged to the PR), and reverting the fallback fails both. The H4 arm text no longer reads as an assignment to bashrs 7.4.1. Self-test 37/37; shell-lint, grep-q and wiring guards PASS. Co-Authored-By: Claude Opus 5.5 --- scripts/check_laya_train_rules.sh | 66 +++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/scripts/check_laya_train_rules.sh b/scripts/check_laya_train_rules.sh index 810cc853bb..6942fe3e17 100755 --- a/scripts/check_laya_train_rules.sh +++ b/scripts/check_laya_train_rules.sh @@ -44,6 +44,7 @@ SIZE_CAP=1048576 REPO="" BASE="origin/main" HEAD_REF="HEAD" +RANGE="" # set by run_rules: base...head, or base..head for a shallow merge head RED=0 usage() { @@ -59,8 +60,8 @@ line() { if [ "$2" = "RED" ]; then RED=1; fi } -changed_names() { g diff --name-only "$BASE...$HEAD_REF"; } -added_names() { g diff --name-only --diff-filter=A "$BASE...$HEAD_REF"; } +changed_names() { g diff --name-only "$RANGE"; } +added_names() { g diff --name-only --diff-filter=A "$RANGE"; } # Paths whose added lines count as "a Makefile, a justfile, a script or a workflow". runner_paths() { @@ -72,7 +73,7 @@ added_runner_lines() { local p while IFS= read -r p; do [ -n "$p" ] || continue - g diff -U0 "$BASE...$HEAD_REF" -- "$p" \ + g diff -U0 "$RANGE" -- "$p" \ | awk -v p="$p" '/^\+\+\+/ {next} /^\+/ {print p ":" substr($0, 2)}' done < <(runner_paths) } @@ -109,7 +110,7 @@ rule_h4() { local exempt touches=$(changed_names | awk -v p="$LT1_PROBE" '/^crates\// && $0 != p' | wc -l) exempt=$(changed_names | awk -v p="$LT1_PROBE" '$0 == p' | wc -l) - local arm="$LT1_RECEIPT base=$on_base head=$on_head; crates/ paths=$touches; exempt $LT1_PROBE=$exempt" + local arm="$LT1_RECEIPT base=$on_base head=$on_head; crates/ paths=$touches; exempt=$exempt ($LT1_PROBE)" if [ "$touches" -gt 0 ] && [ "$on_base" = absent ] && [ "$on_head" = absent ]; then line H4 RED "touches crates/ before LT-1's receipt" "$arm" else @@ -144,7 +145,7 @@ rule_h5() { grep -Eq 'aprender_train::autograd|entrenar::autograd' <<<"$body" && n_ok=$((n_ok + 1)) fi done <<<"$files" - second_tensor=$(g diff -U0 "$BASE...$HEAD_REF" -- '*.rs' | awk ' + second_tensor=$(g diff -U0 "$RANGE" -- '*.rs' | awk ' /^\+\+\+ / {f=$2; next} /^\+[[:space:]]*(pub(\([a-z]+\))?[[:space:]]+)?struct[[:space:]]+Tensor([^A-Za-z0-9_]|$)/ {t[f]=1} /^\+.*fn[[:space:]]+backward[[:space:]]*\(/ {b[f]=1} @@ -193,7 +194,7 @@ rule_h11() { local cli pub new_crates n_new wf bad_wf="" p cli=$(changed_names | awk '$0 == "contracts/apr-cli-commands-v1.yaml"') pub=$(changed_names | awk '/(^|\/)Cargo\.toml$/' | while IFS= read -r p; do - g diff -U0 "$BASE...$HEAD_REF" -- "$p" | awk -v p="$p" '/^[+-][[:space:]]*publish[[:space:]]*=/ {print p; exit}' + g diff -U0 "$RANGE" -- "$p" | awk -v p="$p" '/^[+-][[:space:]]*publish[[:space:]]*=/ {print p; exit}' done | paste -sd ';' -) new_crates=$(added_names | awk '/^crates\/[^\/]+\/Cargo\.toml$/' | paste -sd ';' -) n_new=$(printf '%s' "$new_crates" | awk -F';' 'NF {print NF; exit} END {if (!NR) print 0}') @@ -248,8 +249,25 @@ rule_h15() { run_rules() { g rev-parse -q --verify "$BASE^{commit}" >/dev/null || { echo "base $BASE not found" >&2; exit 2; } g rev-parse -q --verify "$HEAD_REF^{commit}" >/dev/null || { echo "head $HEAD_REF not found" >&2; exit 2; } - printf 'laya-train rules | base %s | head %s\n' \ - "$(g rev-parse --short "$BASE")" "$(g rev-parse --short "$HEAD_REF")" + # A pull request's CI clone is shallow: HEAD is the merge commit and the base is its + # first parent, fetched alone, so `base...head` has no merge base. Then, and only + # when the base IS head's first parent (read from the raw commit, which a shallow + # graft does not hide), the tree diff base..head is the pull request's diff. + # Anything else has no defined diff and stops (exit 2). + local how=merge-base p1 + RANGE="$BASE...$HEAD_REF" + if ! g merge-base "$BASE" "$HEAD_REF" >/dev/null 2>&1; then + p1=$(g cat-file -p "$(g rev-parse "$HEAD_REF")" | awk '/^parent /{n++; if (n == 1) p = $2} /^$/{exit} END{if (n >= 2) print p}') + if [ -n "$p1" ] && [ "$p1" = "$(g rev-parse "$BASE^{commit}")" ]; then + RANGE="$BASE..$HEAD_REF" + how=first-parent + else + echo "no merge base for $BASE...$HEAD_REF and $BASE is not the first parent of a merge head" >&2 + exit 2 + fi + fi + printf 'laya-train rules | base %s | head %s | diff %s\n' \ + "$(g rev-parse --short "$BASE")" "$(g rev-parse --short "$HEAD_REF")" "$how" rule_h1; rule_h2; rule_h4; rule_h5; rule_h10; rule_h11; rule_h12; rule_h15 if [ "$RED" -eq 0 ]; then echo "RESULT PASS"; else echo "RESULT RED"; fi return "$RED" @@ -278,6 +296,32 @@ st_case() { fi } +st_shallow() { + # st_shallow : main moves on (adding a .py the PR does not + # own), the PR runs , and the check runs on a shallow clone of the merge. + local name=$1 expect=$2 setup=$3 out rc=0 got p1 sc + git -C "$ST" checkout -q -B "$name-pr" base + (cd "$ST" && sh -c "$setup") + git -C "$ST" add -A; st_commit "$name pr" + git -C "$ST" checkout -q -B "$name-main" base + echo "y = 1" >"$ST/main_only.py" + git -C "$ST" add -A; st_commit "$name main" + git -C "$ST" -c user.name=t -c user.email=t@t merge -q --no-ff --no-edit "$name-pr" >/dev/null + sc="$ST/shallow-$name" + git clone -q --no-local --depth 1 --branch "$name-main" "$ST" "$sc" 2>/dev/null + p1=$(git -C "$sc" cat-file -p HEAD | awk '/^parent /{print $2; exit}') + git -C "$sc" fetch -q --depth 1 origin "$p1" 2>/dev/null + out=$("$SELF" --repo "$sc" --base "$p1" --head HEAD) || rc=$? + got=$(printf '%s\n' "$out" | awk '$1 == "H1" {print $2; exit}') + if [ "$got" = "$expect" ] && printf '%s\n' "$out" | awk '/diff first-parent/ {f=1} END {exit !f}'; then + printf 'ok %-22s %s %s\n' "$name" H1 "$got" + else + printf 'FAIL %-22s %s want %s got %s rc=%s\n%s\n' "$name" H1 "$expect" "${got:-none}" "$rc" "$out" + ST_FAIL=1 + fi + git -C "$ST" checkout -q base +} + self_test() { SELF=$(cd "$(dirname "$0")" && pwd)/$(basename "$0") ST=$(mktemp -d) @@ -287,6 +331,7 @@ self_test() { GIT_CEILING_DIRECTORIES=$(dirname "$ST") ST_FAIL=0 git -C "$ST" init -q + mkdir -p "$ST/.git/info" && echo "shallow-*/" >>"$ST/.git/info/exclude" mkdir -p "$ST/crates/aprender-decide/src" "$ST/scripts" "$ST/.github/workflows" echo 'fn verify() {}' >"$ST/crates/aprender-decide/src/verify.rs" echo 'echo hi' >"$ST/scripts/a.sh" @@ -324,6 +369,11 @@ self_test() { st_case h15-self-path PASS H15 sh -c "echo '# ${SSH_WORD}is a needle here' >> $H15_SELF" st_case plant-ssh-self-lookalike RED H15 sh -c "echo '${SSH_WORD}host true' >> scripts/check_laya_train_rules2.sh" + # A pull request's CI clone: a depth-1 merge head plus its first parent fetched + # alone, so there is no merge base. Only the pull request's side is judged. + st_shallow plant-py-shallow RED 'echo "x = 1" > x.py' + st_shallow clean-shallow PASS 'echo "# docs" > notes.md' + # H5 and H10 need LT-1's receipt and LT-9's declaration on the base. git -C "$ST" checkout -q base mkdir -p "$ST/evidence/laya-train/lt-1" "$ST/evidence/laya-train/lt-9" From 5804a36ecca8f02b07635893c1742b15421ace61 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 12 Oct 2026 19:41:01 +0200 Subject: [PATCH 10/10] fix(laya-train): LT-0 checker reads a merge queue head (one squashed commit) by its first parent too (Refs #4002) In the merge queue the shallow head is a single squashed commit whose first parent is the base, so the merge-commit-only fallback stopped with exit 2. The fallback now accepts any head whose first parent is the base; two new self-test cases (squash RED and PASS) fail when it is narrowed back. Co-Authored-By: Claude Opus 5.5 --- scripts/check_laya_train_rules.sh | 34 ++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/scripts/check_laya_train_rules.sh b/scripts/check_laya_train_rules.sh index 6942fe3e17..4806822771 100755 --- a/scripts/check_laya_train_rules.sh +++ b/scripts/check_laya_train_rules.sh @@ -249,20 +249,21 @@ rule_h15() { run_rules() { g rev-parse -q --verify "$BASE^{commit}" >/dev/null || { echo "base $BASE not found" >&2; exit 2; } g rev-parse -q --verify "$HEAD_REF^{commit}" >/dev/null || { echo "head $HEAD_REF not found" >&2; exit 2; } - # A pull request's CI clone is shallow: HEAD is the merge commit and the base is its - # first parent, fetched alone, so `base...head` has no merge base. Then, and only - # when the base IS head's first parent (read from the raw commit, which a shallow - # graft does not hide), the tree diff base..head is the pull request's diff. + # A pull request's CI clone is shallow: HEAD is the merge commit (or, in the merge + # queue, the squashed commit) and the base is its first parent, fetched alone, so + # `base...head` has no merge base. Then, and only when the base IS head's first + # parent (read from the raw commit, which a shallow graft does not hide), the tree + # diff base..head is the pull request's diff. # Anything else has no defined diff and stops (exit 2). local how=merge-base p1 RANGE="$BASE...$HEAD_REF" if ! g merge-base "$BASE" "$HEAD_REF" >/dev/null 2>&1; then - p1=$(g cat-file -p "$(g rev-parse "$HEAD_REF")" | awk '/^parent /{n++; if (n == 1) p = $2} /^$/{exit} END{if (n >= 2) print p}') + p1=$(g cat-file -p "$(g rev-parse "$HEAD_REF")" | awk '/^parent /{n++; if (n == 1) p = $2} /^$/{exit} END{if (n >= 1) print p}') if [ -n "$p1" ] && [ "$p1" = "$(g rev-parse "$BASE^{commit}")" ]; then RANGE="$BASE..$HEAD_REF" how=first-parent else - echo "no merge base for $BASE...$HEAD_REF and $BASE is not the first parent of a merge head" >&2 + echo "no merge base for $BASE...$HEAD_REF and $BASE is not the first parent of the head" >&2 exit 2 fi fi @@ -297,16 +298,23 @@ st_case() { } st_shallow() { - # st_shallow : main moves on (adding a .py the PR does not - # own), the PR runs , and the check runs on a shallow clone of the merge. - local name=$1 expect=$2 setup=$3 out rc=0 got p1 sc + # st_shallow : main moves on (adding a .py the + # PR does not own), the PR runs and lands as a merge commit (a pull request + # run) or a single squashed commit (a merge queue run), and the check runs on a shallow + # clone of that head. + local name=$1 expect=$2 how=$3 setup=$4 out rc=0 got p1 sc git -C "$ST" checkout -q -B "$name-pr" base (cd "$ST" && sh -c "$setup") git -C "$ST" add -A; st_commit "$name pr" git -C "$ST" checkout -q -B "$name-main" base echo "y = 1" >"$ST/main_only.py" git -C "$ST" add -A; st_commit "$name main" - git -C "$ST" -c user.name=t -c user.email=t@t merge -q --no-ff --no-edit "$name-pr" >/dev/null + if [ "$how" = squash ]; then + git -C "$ST" merge -q --squash "$name-pr" >/dev/null + st_commit "$name squash" + else + git -C "$ST" -c user.name=t -c user.email=t@t merge -q --no-ff --no-edit "$name-pr" >/dev/null + fi sc="$ST/shallow-$name" git clone -q --no-local --depth 1 --branch "$name-main" "$ST" "$sc" 2>/dev/null p1=$(git -C "$sc" cat-file -p HEAD | awk '/^parent /{print $2; exit}') @@ -371,8 +379,10 @@ self_test() { # A pull request's CI clone: a depth-1 merge head plus its first parent fetched # alone, so there is no merge base. Only the pull request's side is judged. - st_shallow plant-py-shallow RED 'echo "x = 1" > x.py' - st_shallow clean-shallow PASS 'echo "# docs" > notes.md' + st_shallow plant-py-shallow RED merge 'echo "x = 1" > x.py' + st_shallow clean-shallow PASS merge 'echo "# docs" > notes.md' + st_shallow plant-py-squash RED squash 'echo "x = 1" > x.py' + st_shallow clean-squash PASS squash 'echo "# docs" > notes.md' # H5 and H10 need LT-1's receipt and LT-9's declaration on the base. git -C "$ST" checkout -q base