From c6394a2101242b45cbd36257bb6439027c7834a2 Mon Sep 17 00:00:00 2001 From: Jeff Daily Date: Mon, 10 Aug 2026 21:57:39 +0000 Subject: [PATCH 1/6] opencv: keep this branch's project state across the trunk merge --- projects/opencv/notes.md | 18 ++++++++++++++++++ projects/opencv/status.json | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 projects/opencv/notes.md create mode 100644 projects/opencv/status.json diff --git a/projects/opencv/notes.md b/projects/opencv/notes.md new file mode 100644 index 00000000..ef713024 --- /dev/null +++ b/projects/opencv/notes.md @@ -0,0 +1,18 @@ +# opencv notes + +A dependency fork: the OpenCV core the cv::cuda modules in contrib cannot build without. + +## Why the platform row is empty + +This has no test suite of its own. The code is exercised only through the project +that consumes it, so a GPU run against this repository alone would prove nothing. +The empty row is accurate, not a gap in the record. + +The validation lives with **opencv_contrib**, `completed` on linux-gfx1100, linux-gfx90a, windows-gfx1101, windows-gfx1201. + +## Port state + +The `moat-port` branch predates this project being tracked here, so the port exists +but its provenance was not recorded: no plan, no dated validation entry, no note of +which commit was tested. Treat it as real work of unverified state rather than as a +validated port. diff --git a/projects/opencv/status.json b/projects/opencv/status.json new file mode 100644 index 00000000..b9aba78d --- /dev/null +++ b/projects/opencv/status.json @@ -0,0 +1,19 @@ +{ + "schema_version": 3, + "name": "opencv", + "upstream_url": "https://github.com/opencv/opencv", + "fork_url": "https://github.com/AMD-Ecosystem/opencv", + "fork_default_branch": "4.x", + "priority": 0.0, + "ext_type": "unknown", + "adopted_at": "2026-08-06T06:58:03Z", + "updated_at": "2026-08-07T07:05:23Z", + "head_sha": null, + "depends_on": [], + "porting": null, + "waivers": {}, + "license_spdx": "Apache-2.0", + "upstream_repo_id": 5108051, + "stage": "unclaimed", + "platforms": {} +} From 7b068f7f31559eba47a3f13fffe41a2caf7ed449 Mon Sep 17 00:00:00 2001 From: Jeff Daily Date: Tue, 11 Aug 2026 15:56:51 -0700 Subject: [PATCH 2/6] Regenerate README table: colmap PR #4635 and waiver, marian-dev windows validated --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8c0270f0..884b3cf6 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ The project name links upstream. | Project | `wave64` | `wave32` | `windows` | Outcome | | --- | :---: | :---: | :---: | --- | | [3DGS-LM](https://github.com/lukasHoel/3DGS-LM) ([fork](https://github.com/AMD-Ecosystem/3DGS-LM/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#15](https://github.com/lukasHoel/3DGS-LM/pull/15) | -| [3DUNDERWORLD-SLS-GPU_CPU](https://github.com/theICTlab/3DUNDERWORLD-SLS-GPU_CPU) ([fork](https://github.com/AMD-Ecosystem/3DUNDERWORLD-SLS-GPU_CPU/tree/moat-port)) | 🔧 | 🔄 | 🔄 | 🟢 [#33](https://github.com/theICTlab/3DUNDERWORLD-SLS-GPU_CPU/pull/33) | +| [3DUNDERWORLD-SLS-GPU_CPU](https://github.com/theICTlab/3DUNDERWORLD-SLS-GPU_CPU) ([fork](https://github.com/AMD-Ecosystem/3DUNDERWORLD-SLS-GPU_CPU/tree/moat-port)) | ✅ | 🔄 | 🔄 | 🟢 [#33](https://github.com/theICTlab/3DUNDERWORLD-SLS-GPU_CPU/pull/33) | | [3P-ADMM-PC2](https://github.com/Samarvivian/3P-ADMM-PC2) ([fork](https://github.com/AMD-Ecosystem/3P-ADMM-PC2/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#10](https://github.com/Samarvivian/3P-ADMM-PC2/pull/10) | | [accelerated-scan](https://github.com/proger/accelerated-scan) ([fork](https://github.com/AMD-Ecosystem/accelerated-scan/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#17](https://github.com/proger/accelerated-scan/pull/17) | | [aihwkit](https://github.com/IBM/aihwkit) ([fork](https://github.com/AMD-Ecosystem/aihwkit/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#770](https://github.com/IBM/aihwkit/pull/770) | @@ -149,7 +149,7 @@ The project name links upstream. | [amgcl](https://github.com/ddemidov/amgcl) ([fork](https://github.com/AMD-Ecosystem/amgcl/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#315](https://github.com/ddemidov/amgcl/pull/315) | | [anari-visionaray](https://github.com/szellmann/anari-visionaray) ([fork](https://github.com/AMD-Ecosystem/anari-visionaray/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#10](https://github.com/szellmann/anari-visionaray/pull/10) | | [arbor](https://github.com/arbor-sim/arbor) ([fork](https://github.com/AMD-Ecosystem/arbor/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#2512](https://github.com/arbor-sim/arbor/pull/2512) | -| [arrayfire](https://github.com/arrayfire/arrayfire) ([fork](https://github.com/AMD-Ecosystem/arrayfire/tree/moat-port)) | 🔧 | ✅ | ✅ | 🟢 [#3708](https://github.com/arrayfire/arrayfire/pull/3708) | +| [arrayfire](https://github.com/arrayfire/arrayfire) ([fork](https://github.com/AMD-Ecosystem/arrayfire/tree/moat-port)) | ✅ | 🔄 | 🔄 | 🟢 [#3708](https://github.com/arrayfire/arrayfire/pull/3708) | | [AutoDock-GPU](https://github.com/ccsb-scripps/AutoDock-GPU) ([fork](https://github.com/AMD-Ecosystem/AutoDock-GPU/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#320](https://github.com/ccsb-scripps/AutoDock-GPU/pull/320) | | [bam](https://github.com/ZaidQureshi/bam) ([fork](https://github.com/AMD-Ecosystem/bam/tree/moat-port)) | 🚫 | 🚫 | 🚫 | ⏸ on hold | | [barney](https://github.com/NVIDIA/barney) ([fork](https://github.com/AMD-Ecosystem/barney/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#46](https://github.com/NVIDIA/barney/pull/46) | @@ -157,7 +157,7 @@ The project name links upstream. | [bellhopcuda](https://github.com/A-New-BellHope/bellhopcuda) ([fork](https://github.com/AMD-Ecosystem/bellhopcuda/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#65](https://github.com/A-New-BellHope/bellhopcuda/pull/65) | | [brian2cuda](https://github.com/brian-team/brian2cuda) ([fork](https://github.com/AMD-Ecosystem/brian2cuda/tree/moat-port)) | ✅ | ✅ | 🔄 | 🟢 [#327](https://github.com/brian-team/brian2cuda/pull/327) | | [catboost](https://github.com/catboost/catboost) ([fork](https://github.com/AMD-Ecosystem/catboost-moat/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#3111](https://github.com/catboost/catboost/pull/3111) | -| [colmap](https://github.com/colmap/colmap) ([fork](https://github.com/AMD-Ecosystem/colmap/tree/moat-port)) | ✅ | ✅ | ✅ | — | +| [colmap](https://github.com/colmap/colmap) ([fork](https://github.com/AMD-Ecosystem/colmap/tree/moat-port)) | ✅ | ✅ | 🎫 | 🟢 [#4635](https://github.com/colmap/colmap/pull/4635) | | [CPM.cu](https://github.com/OpenBMB/CPM.cu) | 🚫 | — | — | ⚪ not-portable | | [CubbyFlow](https://github.com/utilForever/CubbyFlow) ([fork](https://github.com/AMD-Ecosystem/CubbyFlow/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#145](https://github.com/utilForever/CubbyFlow/pull/145) | | [cuBQL](https://github.com/NVIDIA/cuBQL) ([fork](https://github.com/AMD-Ecosystem/cuBQL/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#35](https://github.com/NVIDIA/cuBQL/pull/35) | @@ -168,7 +168,7 @@ The project name links upstream. | [cuda_voxelizer](https://github.com/Forceflow/cuda_voxelizer) | ⬜ | ⬜ | ⬜ | — | | [cudaKDTree](https://github.com/ingowald/cudaKDTree) ([fork](https://github.com/AMD-Ecosystem/cudaKDTree/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#40](https://github.com/ingowald/cudaKDTree/pull/40) | | [CudaSift](https://github.com/Celebrandil/CudaSift) ([fork](https://github.com/AMD-Ecosystem/CudaSift/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#97](https://github.com/Celebrandil/CudaSift/pull/97) | -| [CuMesh](https://github.com/JeffreyXiang/CuMesh) ([fork](https://github.com/AMD-Ecosystem/CuMesh/tree/moat-port)) | 🔧 | 🔄 | 🔄 | 🟢 [#36](https://github.com/JeffreyXiang/CuMesh/pull/36) | +| [CuMesh](https://github.com/JeffreyXiang/CuMesh) ([fork](https://github.com/AMD-Ecosystem/CuMesh/tree/moat-port)) | ✅ | 🔄 | 🔄 | 🟢 [#36](https://github.com/JeffreyXiang/CuMesh/pull/36) | | [cuPDLP-C](https://github.com/COPT-Public/cuPDLP-C) ([fork](https://github.com/AMD-Ecosystem/cuPDLP-C/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#41](https://github.com/COPT-Public/cuPDLP-C/pull/41) | | [cuPDLPx](https://github.com/MIT-Lu-Lab/cuPDLPx) ([fork](https://github.com/AMD-Ecosystem/cuPDLPx/tree/moat-port)) | ✅ | ✅ | 🔄 | 🟣 [#94](https://github.com/MIT-Lu-Lab/cuPDLPx/pull/94) | | [cupoch](https://github.com/neka-nat/cupoch) ([fork](https://github.com/AMD-Ecosystem/cupoch/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#143](https://github.com/neka-nat/cupoch/pull/143) | @@ -203,7 +203,7 @@ The project name links upstream. | [gaussian_splatting](https://github.com/joeyan/gaussian_splatting) ([fork](https://github.com/AMD-Ecosystem/gaussian_splatting/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#37](https://github.com/joeyan/gaussian_splatting/pull/37) | | [gdtk](https://github.com/gdtk-uq/gdtk) ([fork](https://github.com/AMD-Ecosystem/gdtk/tree/moat-port)) | ✅ | ✅ | ✅ | — | | [GOMC](https://github.com/GOMC-WSU/GOMC) ([fork](https://github.com/AMD-Ecosystem/GOMC/tree/moat-port)) | ✅ | ✅ | ✅ | — | -| [GooFit](https://github.com/GooFit/GooFit) ([fork](https://github.com/AMD-Ecosystem/GooFit/tree/moat-port)) | 🔧 | ✅ | ⬜ | — | +| [GooFit](https://github.com/GooFit/GooFit) ([fork](https://github.com/AMD-Ecosystem/GooFit/tree/moat-port)) | ✅ | ✅ | ⬜ | — | | [gpu4pyscf](https://github.com/pyscf/gpu4pyscf) ([fork](https://github.com/AMD-Ecosystem/gpu4pyscf/tree/moat-port)) | ✅ | ✅ | 🚫 | — | | [GPU_IPC](https://github.com/KemengHuang/GPU_IPC) ([fork](https://github.com/AMD-Ecosystem/GPU_IPC/tree/moat-port)) | ✅ | ✅ | ⬜ | — | | [Gpufit](https://github.com/gpufit/Gpufit) ([fork](https://github.com/AMD-Ecosystem/Gpufit/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#149](https://github.com/gpufit/Gpufit/pull/149) | @@ -228,7 +228,7 @@ The project name links upstream. | [llm.c](https://github.com/karpathy/llm.c) ([fork](https://github.com/AMD-Ecosystem/llm.c/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#854](https://github.com/karpathy/llm.c/pull/854) | | [llmq](https://github.com/IST-DASLab/llmq) | 🚫 | — | — | ⚪ not-portable | | [mahout](https://github.com/apache/mahout) ([fork](https://github.com/AMD-Ecosystem/mahout/tree/moat-port)) | ✅ | ✅ | 🔄 | 🟢 [#1399](https://github.com/apache/mahout/pull/1399) | -| [marian-dev](https://github.com/marian-nmt/marian-dev) ([fork](https://github.com/AMD-Ecosystem/marian-dev/tree/moat-port)) | ✅ | ✅ | 🔄 | 🟢 [#1043](https://github.com/marian-nmt/marian-dev/pull/1043) | +| [marian-dev](https://github.com/marian-nmt/marian-dev) ([fork](https://github.com/AMD-Ecosystem/marian-dev/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#1043](https://github.com/marian-nmt/marian-dev/pull/1043) | | [MASt3R-SLAM](https://github.com/rmurai0610/MASt3R-SLAM) ([fork](https://github.com/AMD-Ecosystem/MASt3R-SLAM/tree/moat-port)) | ✅ | ✅ | ✅ | — | | [mcx](https://github.com/fangq/mcx) ([fork](https://github.com/AMD-Ecosystem/mcx/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#264](https://github.com/fangq/mcx/pull/264) | | [metaeuk](https://github.com/soedinglab/metaeuk) ([fork](https://github.com/AMD-Ecosystem/metaeuk/tree/moat-port)) | ✅ | ✅ | ✅ | — | @@ -255,7 +255,7 @@ The project name links upstream. | [Quest](https://github.com/mit-han-lab/Quest) ([fork](https://github.com/AMD-Ecosystem/Quest/tree/moat-port)) | ✅ | ✅ | ⬜ | — | | [QUICK](https://github.com/merzlab/QUICK) ([fork](https://github.com/AMD-Ecosystem/QUICK/tree/moat-port)) | ✅ | ✅ | 🚫 | — | | [rmagine](https://github.com/uos/rmagine) ([fork](https://github.com/AMD-Ecosystem/rmagine/tree/moat-port)) | ✅ | ✅ | ✅ | — | -| [rmcl](https://github.com/uos/rmcl) ([fork](https://github.com/AMD-Ecosystem/rmcl/tree/moat-port)) | ⬜ | ⬜ | ⬜ | — | +| [rmcl](https://github.com/uos/rmcl) ([fork](https://github.com/AMD-Ecosystem/rmcl/tree/moat-port)) | 🔧 | ⬜ | ⬜ | — | | [RWKV-CUDA](https://github.com/BlinkDL/RWKV-CUDA) ([fork](https://github.com/AMD-Ecosystem/RWKV-CUDA/tree/moat-port)) | ✅ | ✅ | ✅ | — | | [RXMesh](https://github.com/owensgroup/RXMesh) ([fork](https://github.com/AMD-Ecosystem/RXMesh/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#73](https://github.com/owensgroup/RXMesh/pull/73) | | [SCAMP](https://github.com/zpzim/SCAMP) ([fork](https://github.com/AMD-Ecosystem/SCAMP/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#145](https://github.com/zpzim/SCAMP/pull/145) | From e847e57369d63bc2769c369bf4387359f713883f Mon Sep 17 00:00:00 2001 From: Jeff Daily Date: Wed, 12 Aug 2026 13:16:19 -0700 Subject: [PATCH 3/6] flow: stage maintainer-requested fixes off open upstream PRs (#24) * flow: stage maintainer-requested fixes off open upstream PRs Once an upstream PR is open, its head branch (moat-port) is upstream-visible: any push lands in front of the maintainer before review, revalidation, or a person's approval. The colmap #4635 maintainer round nearly shipped an unreviewed fix this way. This closes the hole: - fixes stage on moat-fix- (moatlib fix-branch); head_sha tracks the staging tip so revalidate/pr-gate derivation is unchanged; new published_sha records what the open PR shows - a person approves the delta on a fork review PR (upstream.py --fix-review); a body section headed '## Upstream reply' is approved with it - upstream.py --merge-fix --apply re-checks the live approval and all gates, fast-forwards moat-port to exactly the approved tip, posts the approved reply, deletes the staging branch, and records it -- the second pre-authorized upstream write, same contract as --publish --apply - enforcement: a fork-clone pre-push hook (moatlib protect-fork, installed by orient), a check.py 'published' gate, and a HEAD-MOVED report in the --dry-run reconciler (a maintainer push is reviewed and recommended on, never auto-absorbed) Docs: AGENTS.md, moat-checkup skill, porter/reviewer/validator roles, CONTRIBUTING.md, VISUAL.md. Schema regenerated; README table regenerated. Verified with a synthetic project and local bare fork: hook refusal/bypass, gate detection, fix-branch backfill, fix-ready refusals, report paths. Co-Authored-By: Claude Fable 5 * flow: make the fix-flow enforcement hold where the records live Review of the staged fix flow found the enforcement layer passing in the configuration MOAT actually runs in, and the merge path leaving residue a person then has to untangle. Fixes, in the order they bite: - the fork pre-push hook read projects//status.json as a PATH, but an in-flight project's folder lives on port/ and is absent from every checkout standing anywhere else. json.load then raised, the state read as empty, and the hook exited 0 -- so it allowed exactly the push it exists to refuse. Verified on the gfx1151 host: projects/colmap/status.json is absent while projects/colmap/src is present, and colmap #4635 is the round this flow is meant to run first. The hook now asks `moatlib.py pr-state ` (project_record, so it resolves from whichever ref holds the record) and REFUSES when the state cannot be read: a guard that cannot tell must not be the reason an unreviewed commit reaches a maintainer. It reads the ref updates first, so only a push to the PR branch pays for the lookup, and --refresh re-fetches so another host's `set-pr-open` cannot be missed. - protect-fork now returns a level and sends "a NON-moat pre-push hook is installed; not touching it" to stderr, which orient no longer discards. An unprotected clone that looks exactly like a protected one is the failure mode being guarded against. - the 'published' gate compared the clone's LOCAL port branch to published_sha. --merge-fix pushes a sha to the fork and moves no local ref, so the host that performed the approved merge failed this gate on every subsequent push (install_hooks' pre-push runs check.py --fast, which includes it), and any host that had not fetched since a merge failed it too. It now asks the only question a clone can answer -- is it AHEAD of the published tip, which is the actual precursor to an unapproved push. Behind is a stale fetch and passes silently; diverged is reported as a rewrite. do_merge_fix also fast-forwards the clone's own branch afterwards. - fix-branch was unreachable for most projects it applies to: 30 of the 53 open-PR records are trunk-resident with no port/, so save_record raised "check out main", which rule 1 forbids and orient refuses -- and belongs_on_branch only flips once head_sha moves, which cannot happen until the round is recorded. It now re-homes the folder onto port/ first (ensure_port_branch): the fix round IS the event that means "in flight again", so it is what breaks the circle. - record_writable_here is the same question asked in advance, and both --fix-review and --merge-fix now pre-flight it. Discovering an unwritable record AFTER the upstream PR has moved is the worst state this flow can produce; --merge-fix reports HELD instead. - do_merge_fix records the merge BEFORE deleting the staging branch. A failed delete leaves a spare branch; a failed record after a delete left a moved PR, a published_sha naming the old tip, and a fix block pointing at a branch that no longer existed -- which --dry-run then reports as a maintainer push. set-fix-merged is exposed for that recovery (it records, it authorises nothing). - open_fix_review_pr no longer lets set_fix_review_pr raise past a created PR: the URL was lost, the round still read as needing a review PR, and the next run would open a second one. - the delta jargon scan treated any parsed compare as complete. `compare` caps at 250 commits and 300 files and omits `patch` on large ones, so a truncated answer read as clean on the last check before in-house vocabulary reaches a stranger's repository. It now scans from the fork clone when one holds both commits, and raises on a truncated API answer. - fix_branch backfilled published_sha from head_sha unverified, though that value becomes the merge-time ancestry baseline AND the baseline --dry-run calls a maintainer push. It now confirms against the live PR head and refuses when they disagree, which is the HEAD-MOVED case. - the '## Upstream reply' section ran to the end of the body, so a trailing '## Notes' written for our own eyes would have been posted verbatim upstream. It now ends at the next ## heading, skipping fenced blocks. - poll() compared shas with != while everything else uses same_commit; an abbreviated published_sha would have reported a maintainer push every sweep. set_fix_review_pr and set_fix_merged also switched from load_status to project_record, so a port branch's copy of somebody else's folder cannot be edited and written back over the real record. Verified: the hook refuses/allows correctly across 6 cases against a local bare fork, including the branch-resident record that defeated v1 and the unreadable-state refusal (v1 allowed both); 27 logic checks over the reply extraction, compare truncation, local delta scan, residency helpers; 7 over the published gate's ahead/behind/diverged/stale-object paths; 8 over the branch sync. Full check.py green apart from the pre-existing gh-shim PATH artifact on this host. --------- Co-authored-by: Claude Fable 5 --- .claude/agents/porter.md | 6 +- .claude/agents/reviewer.md | 2 +- .claude/agents/validator.md | 2 +- .claude/skills/moat-checkup/SKILL.md | 83 +++- AGENTS.md | 6 +- CONTRIBUTING.md | 12 + README.md | 10 +- VISUAL.md | 6 + schema/status.schema.json | 38 ++ utils/check.py | 64 ++++ utils/gen_schema.py | 18 + utils/moatlib.py | 455 +++++++++++++++++++++- utils/orient.sh | 5 + utils/upstream.py | 540 ++++++++++++++++++++++++++- 14 files changed, 1206 insertions(+), 41 deletions(-) diff --git a/.claude/agents/porter.md b/.claude/agents/porter.md index d6f36c16..489eca1d 100644 --- a/.claude/agents/porter.md +++ b/.claude/agents/porter.md @@ -18,7 +18,9 @@ If another arch holds the lock, `set-state ... porting` refuses and names the ho ## Steps 1. The fork must already exist. **You cannot create one** -- fork creation in the org is admin-only, and a person creates it. If `fork_url` is unset or the repo is missing and you were dispatched at `planned`, set `awaiting-fork` and stop; do not run `gh repo fork`. From any later state (`changes-requested`, `validation-failed`, a resumed `porting`) the fork already existed once, so its absence is an anomaly the state machine deliberately refuses to record as `awaiting-fork` -- stop and report it instead. -2. Ensure projects//src/ has the fork from `status.json.fork_url` as a remote. Put the port on a `moat-port` topic branch; the fork's default branch stays a clean mirror of upstream. The single upstream PR is `moat-port` -> upstream default. +2. Ensure projects//src/ has the fork from `status.json.fork_url` as a remote, and run `python3 utils/moatlib.py protect-fork ` on a fresh clone. Put the port on a `moat-port` topic branch; the fork's default branch stays a clean mirror of upstream. The single upstream PR is `moat-port` -> upstream default. + + **If `status.json.pr_state` is `open`, `moat-port` is frozen** -- it is the open PR's head, and a push to it lands in front of the maintainer before anyone reviewed it. Work on the staging branch `python3 utils/moatlib.py fix-branch ` reports, cut from the `moat-port` tip. Push ONLY that branch, keep it a strict descendant of `moat-port` (append commits; never rebase or amend anything at or below the published tip), and leave the merge to `upstream.py --merge-fix` after a person approves the delta. 3. Apply plan.md. Strategy A: add the single `cuda_to_hip.h` compat header, `enable_language(HIP)` + `set_source_files_properties(... LANGUAGE HIP)`, keep other files in CUDA spelling. Strategy B: rely on torch build-time hipify; fix only what hipify cannot. 4. Honor the fault classes (`cuda-to-rocm` skill): a warp_size abstraction (never literal 32), rule-of-five on texture/resource handles, clamp OOB neighbor reads, 256B texture pitch, library swaps. Any fix to shared (non-arch-guarded) code MUST be arch-unified (correct on wave32 AND wave64), never a per-arch hack that ping-pongs platforms. 5. Build for the detected arch, wrapped: `utils/timeit.sh compile -- `. @@ -26,7 +28,7 @@ If another arch holds the lock, `set-state ... porting` refuses and names the ho The message is upstream-visible, so it carries NO in-house vocabulary -- no "lead"/"follower", "Strategy A/B", "head_sha", "moat-port", or "MOAT". Verify before pushing: `python3 utils/jargon.py --port `, which scans the whole branch -- not the commit you just wrote, because everything already on it ships too. Say "a compatibility header", not "Strategy A"; name the GPU, not "the lead platform". This has caused real review churn. 7. **Document the ROCm build. This is part of the port, not a later step** -- there is no PR-prep phase to catch it, and the validator will hold the arch if it is missing. Document it wherever the project documents its CUDA build, in the project's HOUSE STYLE. Check EVERY doc location (README, `docs/`, Sphinx/`.rst` install guides, doc sites), not just the README: a project whose README or install guide carries a CUDA build block gets the parallel `USE_HIP` / `PYTORCH_ROCM_ARCH` block in the SAME place, while a landing-page README that defers build steps to an external doc site gets a brief AMD-support note in its descriptive style, NOT an imposed build-command block. Never add build steps a project deliberately keeps elsewhere. -8. Record the new fork HEAD: `python3 utils/moatlib.py advance-head `. Any arch whose `validated_sha` is now behind reads `revalidate` from that moment -- the staleness follows from the two shas rather than being written down, so nothing can forget to. **This is also what hands a failed arch back to a validator**, by the same rule read the other way: an arch that recorded `validation-failed` is yours only while the sha it failed at is still the head, so forgetting this step leaves the fix invisible and the arch is dispatched to you again. Append gotchas to notes.md. If a gotcha would help someone porting a DIFFERENT project, promote it to the `cuda-to-rocm` skill's `references/` in the same change, naming this project as the source -- a lesson left only in notes.md is invisible to the next porter. +8. Record the new fork HEAD: `python3 utils/moatlib.py advance-head `. In a fix round this is the STAGING tip and advancing to it is correct -- completed arches flip to `revalidate` so the evidence exists before the delta ever reaches the open PR. Any arch whose `validated_sha` is now behind reads `revalidate` from that moment -- the staleness follows from the two shas rather than being written down, so nothing can forget to. **This is also what hands a failed arch back to a validator**, by the same rule read the other way: an arch that recorded `validation-failed` is yours only while the sha it failed at is still the head, so forgetting this step leaves the fix invisible and the arch is dispatched to you again. Append gotchas to notes.md. If a gotcha would help someone porting a DIFFERENT project, promote it to the `cuda-to-rocm` skill's `references/` in the same change, naming this project as the source -- a lesson left only in notes.md is invisible to the next porter. ## State transitions - `planned` / `changes-requested` / `validation-failed`: go to `porting` (which takes the lock) while working, then `ported` once it builds and is pushed. diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md index 8b12cb08..ab1464aa 100644 --- a/.claude/agents/reviewer.md +++ b/.claude/agents/reviewer.md @@ -8,7 +8,7 @@ model: opus You are the MOAT reviewer. You review the ported fork branch before validation. You post nothing to any upstream repo. ## Steps -1. Invoke the /pr-review skill in local-branch mode against the fork branch in projects//src/ (review `git diff ...HEAD`). +1. Invoke the /pr-review skill in local-branch mode against the fork branch in projects//src/ (review `git diff ...HEAD`). When status.json carries a `fix` block (a fix round on an open upstream PR), the working branch is `fix.branch` rather than `moat-port` and the review scope is the delta, `git diff ...HEAD`. 2. Beyond the skill's checklist, verify the ROCm fault classes: no hardcoded 32 / wrong warpSize assumptions, rule-of-five on texture/resource handles, clamped OOB neighbor reads, 256B texture pitch, the correct Strategy A vs B for the build type, arch-unified (not per-arch) fixes to shared code, library swaps, commit-message rules (`[ROCm]` title, no noreply trailer), and no AMD-internal account references. 3. The pr-review skill fact-checks every finding before it is reported; follow it. When dispatched as a child you may not be able to dispatch children of your own, so run the diff --git a/.claude/agents/validator.md b/.claude/agents/validator.md index d0d5d087..1c2fd1d4 100644 --- a/.claude/agents/validator.md +++ b/.claude/agents/validator.md @@ -12,7 +12,7 @@ Scope: you validate exactly the ONE arch you were dispatched for. PR-readiness i You need no lock: validation is read-only on code and writes only your own arch's record, which the merge driver handles concurrently. Do not take the `porting` lock, and do not wait on it unless you intend to change code. ## Steps -1. Build the fork branch for the detected arch, wrapped: `utils/timeit.sh compile -- `. +1. Build the fork branch for the detected arch, wrapped: `utils/timeit.sh compile -- `. When status.json carries a `fix` block (a fix round on an open upstream PR), the working branch is `fix.branch` rather than `moat-port`; check out `head_sha` from it. Your evidence at that tip is what lets the delta merge before it ever reaches the open PR. 2. Run the project's real test suite, GPU tests in focus, wrapped: `utils/timeit.sh test -- `. Confirm the non-GPU tests do not regress versus the upstream baseline. 3. CUDA no-regression gate -- prove the port still compiles as CUDA. This tests the CODE, not the arch, so it runs ONCE per head_sha: skip it if notes.md already records the CUDA gate at this head_sha, on carried-forward revalidations, and on any host without the CUDA toolkit (in practice the Windows hosts, so it lands on whichever Linux arch validates first). Compiling CUDA needs no NVIDIA GPU or driver, only the toolkit -- use nvcc from the dedicated conda env (`/opt/conda/envs/cuda-12.8/bin/nvcc`; if the env is missing, create it: `conda create -y -n cuda-12.8 -c nvidia cuda-toolkit=12.8`; host gcc 13 works). ALWAYS pin the arch (`-DCMAKE_CUDA_ARCHITECTURES=80`): `native` autodetection on a host with no NVIDIA GPU silently degrades to an ancient arch, and `atomicAdd(double*)` "no instance of overloaded function" is the fingerprint of that, not a real failure. Some projects hardcode `CUDA_ARCHITECTURES native` in cmake target properties where a `-D` does not reach (grep for `CUDA_ARCHITECTURES` if the pin does not take); patch that locally as a throwaway (discard before completion) unless an override knob genuinely belongs in the PR. Wrap in `utils/timeit.sh cuda-compile -- `. - Port build fails -> build the UPSTREAM base sha with the identical toolchain and arch. Identical errors upstream = pre-existing breakage, record verbatim in notes, not a gate. Errors only on the port = CUDA regression -> validation-failed back to the porter; the CUDA build must be a pure passthrough. Typical regression shapes: a type-alias/namespace define added for HIP but not defined in the CUDA branch (cpx silently becoming std::complex while math stays cuda::std), and deleted "workaround" code the CUDA path still needs (bellhopcuda shipped both). diff --git a/.claude/skills/moat-checkup/SKILL.md b/.claude/skills/moat-checkup/SKILL.md index 31adcbbe..cc931723 100644 --- a/.claude/skills/moat-checkup/SKILL.md +++ b/.claude/skills/moat-checkup/SKILL.md @@ -23,20 +23,25 @@ would hand everything back anyway -- and the round trip costs more than doing it bash utils/orient.sh # approved ports, fork releases, next work python3 utils/upstream.py --review # finished ports with no review PR open python3 utils/upstream.py --attention # who is waiting on us + python3 utils/upstream.py --fix-review # staged fix rounds with no review PR open + python3 utils/upstream.py --merge-fix # approved fix rounds ready to fast-forward python3 utils/upstream.py --approvals # approvals overtaken by a push or a body edit - python3 utils/upstream.py --dry-run # where our record disagrees with GitHub + python3 utils/upstream.py --dry-run # where our record disagrees with GitHub (incl. a moved PR head) python3 utils/moatlib.py waivers # gate waivers waiting on a maintainer python3 utils/deferred.py pending # deferrals nobody has ruled on -The first is section 0 and comes before everything else: a maintainer who asked us to -stop is the one item here where continuing to work is worse than doing nothing. The -second names any port whose approval is standing and whose gates are met. The third -is where work piles up: a port cannot be approved until its review PR exists, and -nothing opens one automatically, so ports sit finished and unreviewable -- the report -names them all; do not trust any remembered count. -`--review --apply --name

--title '' --body-file ` opens one. The fourth lists open PRs where a maintainer asked for something, had the last word, -or has gone quiet. The fifth catches a review GitHub still shows as green over content -nobody approved. The sixth is bookkeeping. The seventh is section 6: a waiver nobody has +The opt-out listing is section 0 and comes before everything else: a maintainer who +asked us to stop is the one item here where continuing to work is worse than doing +nothing. `orient.sh` names any port whose approval is standing and whose gates are +met. `--review` is where work piles up: a port cannot be approved until its review PR +exists, and nothing opens one automatically, so ports sit finished and unreviewable -- +the report names them all; do not trust any remembered count. +`--review --apply --name

--title '' --body-file ` opens one. `--attention` +lists open PRs where a maintainer asked for something, had the last word, or has gone +quiet. `--fix-review` and `--merge-fix` are the two ends of a staged fix round +(section 2). `--approvals` catches a review GitHub still shows as green over content +nobody approved. `--dry-run` is bookkeeping, and a HEAD-MOVED line in it is section +2's maintainer-push case. The waiver listing is section 6: a waiver nobody has answered is a finished port that cannot be submitted. Nothing runs on a schedule. This checkup IS the sweep, so the record only reconciles when @@ -184,14 +189,56 @@ This is the bulk of the work and where the value is. - Read the whole thread before responding, including review comments on specific lines (`gh pr view --repo --json comments,reviews`). -- Distinguish a request for a code change (route to the porter: set state `porting`, - which takes the fork-write lock -- `changes-requested` is not reachable from - `review-passed`, the stage an open upstream PR sits at) from a question you can - answer. -- When a fix lands, the fork HEAD moves, which flips validated platforms to `revalidate`. - That is correct and expected -- do not suppress it. -- Reply tone: plain and short. No "happy to...", no employer name-dropping, nothing - lawyerly. Answer the question asked. +- Distinguish a request for a code change from a question you can answer. + +A code change goes through the STAGED FIX FLOW, because the open PR's branch is +upstream-visible and a push to it lands in front of the maintainer before anyone here +has reviewed, revalidated, or approved it: + +1. Route to the porter: set state `porting` (takes the fork-write lock; + `changes-requested` is not reachable from `review-passed`, the stage an open + upstream PR sits at), and `moatlib.py fix-branch ` names the staging branch + -- `moat-fix-`, cut from the published tip. The porter pushes ONLY that + branch and advances `head_sha` to its tip. + + `fix-branch` also puts the project's folder back on `port/` if a finished + port had left it on the trunk, because the round is about to write records and + the trunk is protected -- most open-PR records are trunk-resident, so expect that + line and let it happen. And it confirms against GitHub that what the record calls + the head is what the PR actually shows before it writes `published_sha`; if it + refuses because those disagree, that is the HEAD-MOVED case below, not a hiccup. +2. The head move flips validated platforms to `revalidate` -- correct and expected; + the evidence is gathered at the staging tip, BEFORE anything is visible upstream. +3. Reviewer reviews the delta; validators revalidate; then + `upstream.py --fix-review --apply --name

--title '' --body-file ` opens + the fork review PR (`moat-fix-` -> `moat-port`). Its diff is the delta; its + title and body are NOT republished upstream, so say plainly what the maintainer + asked, what changed, and what revalidated. A body section headed exactly + `## Upstream reply` is the one upstream-visible part: approve it and it is posted + verbatim on the upstream PR after the merge. That section ends at the next `##` + heading, so anything written for our own eyes goes after one. +4. `/moat approve` on that PR, then `upstream.py --merge-fix --apply` re-checks the + live approval and every gate, fast-forwards `moat-port` to exactly the approved + tip, posts the approved reply, records `published_sha`, and deletes the staging + branch. Same contract as `--publish --apply`: mechanical because a person + approved exactly this content. Do not push `moat-port` by hand -- the fork's + pre-push hook (`moatlib.py protect-fork`) refuses it while the PR is open. + + Run it from a host that has the fork clone AND can write the record; it reports + HELD rather than moving the PR when either is missing, because the one thing this + must never leave behind is an upstream PR that moved and a record that cannot say + so. + +A question needing no code change is answered in a comment: draft it, show it, wait -- +or fold it into the next fix round's `## Upstream reply` so one approval covers it. +Reply tone: plain and short. No "happy to...", no employer name-dropping, nothing +lawyerly. Answer the question asked. + +`--dry-run` reporting HEAD-MOVED for an open PR means its branch moved outside this +flow -- usually a maintainer editing our branch, which they did on purpose. Read what +landed, review the commit(s), and put a recommendation in front of a person; never +auto-absorb or revert it. If the content is accepted, a fresh fix round from the new +tip re-enters the flow. If a maintainer signals they will not take the contribution, stop and record it rather than pushing. Record it with `moatlib.py set-pr-closed --note ""`, and if the diff --git a/AGENTS.md b/AGENTS.md index d5077c03..449f7ab8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,11 +31,13 @@ There is no lead platform. The shared `porting` record serializes the exclusive - `port/` in this MOAT repository holds the project's control-plane records while work is outstanding. Completed records reach `main` through review. - `moat-port` in `AMD-Ecosystem/` holds the actual source port. The fork default branch remains an unmodified upstream mirror. +Once the upstream PR is open, `moat-port` is upstream-visible and frozen: any push to it changes the PR in front of the maintainer. Maintainer-requested fixes stage on `moat-fix-` (`moatlib.py fix-branch`), cut from the published tip and never rebased; the normal porter/reviewer/validator cycle runs against the staging tip, `head_sha` follows it, and `published_sha` records what the PR shows. A fix round means the project is in flight again, so `fix-branch` also re-homes a trunk-resident folder onto `port/` before its first write. Only `upstream.py --merge-fix --apply` moves `moat-port`, after a person approves the delta on a fork review PR (`upstream.py --fix-review`). A pre-push hook in each fork clone (`moatlib.py protect-fork`, installed by orient) enforces the freeze, and refuses the push when it cannot determine the PR state rather than assuming there is none. + Use `moatlib.project_record`, `all_projects`, `fleet`, or the corresponding CLI commands when asking what MOAT knows. A project folder may live on another ref, so scanning the checked-out `projects/` directory alone can produce the opposite answer. # Human decisions and external writes -Agents may edit MOAT records and fork source, build and test locally, push MOAT project branches and `moat-port` branches, and perform read-only GitHub queries. +Agents may edit MOAT records and fork source, build and test locally, push MOAT project branches and fork port branches (while no upstream PR is open, `moat-port` itself; while one is open, only the `moat-fix-` staging branch), and perform read-only GitHub queries. The following require a person's explicit decision: @@ -47,7 +49,7 @@ The following require a person's explicit decision: An upstream maintainer asking MOAT to stop is the one decision an agent may record directly with `utils/optout.py record`, because it is their decision and can only reduce work. Never argue or ask for a reason. -The one pre-authorized upstream write is `python3 utils/upstream.py --publish --apply`: it may open the upstream PR only after the review PR on our fork contains the exact code, title, and body a person approved and the tool rechecks the live approval and all gates. Do not reproduce that write with an ad-hoc `gh` command. +Two upstream writes are pre-authorized, both on the same contract -- a person approved exactly this content on a fork review PR, and the tool rechecks the live approval and all gates before acting. `python3 utils/upstream.py --publish --apply` opens the upstream PR with the approved code, title, and body. `python3 utils/upstream.py --merge-fix --apply` fast-forwards an open PR's branch to an approved fix round's tip, and posts the approved reply when the fix review PR's body carries an `## Upstream reply` section. Do not reproduce either write with an ad-hoc `gh` command. `utils/gh_guard.py`, installed by `utils/install_hooks.py`, blocks obvious forbidden GitHub writes from either harness. It is defense in depth, not a substitute for scoped credentials. Do not bypass it, call the real `gh` binary, use `curl`, or otherwise route around a refusal. The trusted publisher resolves the real binary itself only after its approval checks pass. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d37bafc1..0cee53fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -172,6 +172,18 @@ approval, and only a person can approve. Everything after that first post -- replies to maintainers, follow-up comments, a re-request for review -- is its own act and needs its own yes. +A maintainer asking for a code change starts a **fix round**, which repeats this whole +shape one level down. The open PR's branch is frozen (a push to it lands in front of the +maintainer unreviewed; a pre-push hook in the fork clone refuses it), so the fix stages on +`moat-fix-`, is reviewed and revalidated at the staging tip, and gets its own fork +review PR (`upstream.py --fix-review`) whose diff is exactly the delta. Approving it with +`/moat approve` authorizes `upstream.py --merge-fix --apply` to fast-forward the PR branch +to exactly the approved tip -- and to post, verbatim, the reply drafted under the body's +`## Upstream reply` heading, so the code and the words arrive together and one approval +covers both. The title and body of a fix review PR are not republished anywhere; only that +reply section is, and it ends at the next `##` heading so that what is written for our own +eyes can sit in the same body without travelling with it. + ## Who opens the upstream pull request An attended session on the maintainer's own machine, running one command -- the diff --git a/README.md b/README.md index 884b3cf6..87ca56c6 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ The project name links upstream. | Project | `wave64` | `wave32` | `windows` | Outcome | | --- | :---: | :---: | :---: | --- | | [3DGS-LM](https://github.com/lukasHoel/3DGS-LM) ([fork](https://github.com/AMD-Ecosystem/3DGS-LM/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#15](https://github.com/lukasHoel/3DGS-LM/pull/15) | -| [3DUNDERWORLD-SLS-GPU_CPU](https://github.com/theICTlab/3DUNDERWORLD-SLS-GPU_CPU) ([fork](https://github.com/AMD-Ecosystem/3DUNDERWORLD-SLS-GPU_CPU/tree/moat-port)) | ✅ | 🔄 | 🔄 | 🟢 [#33](https://github.com/theICTlab/3DUNDERWORLD-SLS-GPU_CPU/pull/33) | +| [3DUNDERWORLD-SLS-GPU_CPU](https://github.com/theICTlab/3DUNDERWORLD-SLS-GPU_CPU) ([fork](https://github.com/AMD-Ecosystem/3DUNDERWORLD-SLS-GPU_CPU/tree/moat-port)) | ✅ | ✅ | 🔄 | 🟢 [#33](https://github.com/theICTlab/3DUNDERWORLD-SLS-GPU_CPU/pull/33) | | [3P-ADMM-PC2](https://github.com/Samarvivian/3P-ADMM-PC2) ([fork](https://github.com/AMD-Ecosystem/3P-ADMM-PC2/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#10](https://github.com/Samarvivian/3P-ADMM-PC2/pull/10) | | [accelerated-scan](https://github.com/proger/accelerated-scan) ([fork](https://github.com/AMD-Ecosystem/accelerated-scan/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#17](https://github.com/proger/accelerated-scan/pull/17) | | [aihwkit](https://github.com/IBM/aihwkit) ([fork](https://github.com/AMD-Ecosystem/aihwkit/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#770](https://github.com/IBM/aihwkit/pull/770) | @@ -149,7 +149,7 @@ The project name links upstream. | [amgcl](https://github.com/ddemidov/amgcl) ([fork](https://github.com/AMD-Ecosystem/amgcl/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#315](https://github.com/ddemidov/amgcl/pull/315) | | [anari-visionaray](https://github.com/szellmann/anari-visionaray) ([fork](https://github.com/AMD-Ecosystem/anari-visionaray/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#10](https://github.com/szellmann/anari-visionaray/pull/10) | | [arbor](https://github.com/arbor-sim/arbor) ([fork](https://github.com/AMD-Ecosystem/arbor/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#2512](https://github.com/arbor-sim/arbor/pull/2512) | -| [arrayfire](https://github.com/arrayfire/arrayfire) ([fork](https://github.com/AMD-Ecosystem/arrayfire/tree/moat-port)) | ✅ | 🔄 | 🔄 | 🟢 [#3708](https://github.com/arrayfire/arrayfire/pull/3708) | +| [arrayfire](https://github.com/arrayfire/arrayfire) ([fork](https://github.com/AMD-Ecosystem/arrayfire/tree/moat-port)) | ✅ | ✅ | 🔄 | 🟢 [#3708](https://github.com/arrayfire/arrayfire/pull/3708) | | [AutoDock-GPU](https://github.com/ccsb-scripps/AutoDock-GPU) ([fork](https://github.com/AMD-Ecosystem/AutoDock-GPU/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#320](https://github.com/ccsb-scripps/AutoDock-GPU/pull/320) | | [bam](https://github.com/ZaidQureshi/bam) ([fork](https://github.com/AMD-Ecosystem/bam/tree/moat-port)) | 🚫 | 🚫 | 🚫 | ⏸ on hold | | [barney](https://github.com/NVIDIA/barney) ([fork](https://github.com/AMD-Ecosystem/barney/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#46](https://github.com/NVIDIA/barney/pull/46) | @@ -168,7 +168,7 @@ The project name links upstream. | [cuda_voxelizer](https://github.com/Forceflow/cuda_voxelizer) | ⬜ | ⬜ | ⬜ | — | | [cudaKDTree](https://github.com/ingowald/cudaKDTree) ([fork](https://github.com/AMD-Ecosystem/cudaKDTree/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#40](https://github.com/ingowald/cudaKDTree/pull/40) | | [CudaSift](https://github.com/Celebrandil/CudaSift) ([fork](https://github.com/AMD-Ecosystem/CudaSift/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#97](https://github.com/Celebrandil/CudaSift/pull/97) | -| [CuMesh](https://github.com/JeffreyXiang/CuMesh) ([fork](https://github.com/AMD-Ecosystem/CuMesh/tree/moat-port)) | ✅ | 🔄 | 🔄 | 🟢 [#36](https://github.com/JeffreyXiang/CuMesh/pull/36) | +| [CuMesh](https://github.com/JeffreyXiang/CuMesh) ([fork](https://github.com/AMD-Ecosystem/CuMesh/tree/moat-port)) | ✅ | ✅ | 🔄 | 🟢 [#36](https://github.com/JeffreyXiang/CuMesh/pull/36) | | [cuPDLP-C](https://github.com/COPT-Public/cuPDLP-C) ([fork](https://github.com/AMD-Ecosystem/cuPDLP-C/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#41](https://github.com/COPT-Public/cuPDLP-C/pull/41) | | [cuPDLPx](https://github.com/MIT-Lu-Lab/cuPDLPx) ([fork](https://github.com/AMD-Ecosystem/cuPDLPx/tree/moat-port)) | ✅ | ✅ | 🔄 | 🟣 [#94](https://github.com/MIT-Lu-Lab/cuPDLPx/pull/94) | | [cupoch](https://github.com/neka-nat/cupoch) ([fork](https://github.com/AMD-Ecosystem/cupoch/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#143](https://github.com/neka-nat/cupoch/pull/143) | @@ -212,7 +212,7 @@ The project name links upstream. | [gRASPA](https://github.com/snurr-group/gRASPA) ([fork](https://github.com/AMD-Ecosystem/gRASPA/tree/moat-port)) | ✅ | ✅ | ✅ | — | | [gtsam_points](https://github.com/koide3/gtsam_points) ([fork](https://github.com/AMD-Ecosystem/gtsam_points/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#99](https://github.com/koide3/gtsam_points/pull/99) | | [h2o4gpu](https://github.com/h2oai/h2o4gpu) | ⬜ | ⬜ | ⬜ | — | -| [HEonGPU](https://github.com/Alisah-Ozcan/HEonGPU) ([fork](https://github.com/AMD-Ecosystem/HEonGPU/tree/moat-port)) | ✅ | ⬜ | ⬜ | — | +| [HEonGPU](https://github.com/Alisah-Ozcan/HEonGPU) ([fork](https://github.com/AMD-Ecosystem/HEonGPU/tree/moat-port)) | ✅ | ✅ | ⬜ | — | | [icicle](https://github.com/ingonyama-zk/icicle) ([fork](https://github.com/AMD-Ecosystem/icicle/tree/moat-port)) | ✅ | ✅ | 🚫 | — | | [k2](https://github.com/k2-fsa/k2) ([fork](https://github.com/AMD-Ecosystem/k2/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#1353](https://github.com/k2-fsa/k2/pull/1353) | | [kaldi](https://github.com/kaldi-asr/kaldi) ([fork](https://github.com/AMD-Ecosystem/kaldi/tree/moat-port)) | ✅ | ✅ | 🚫 | 🟢 [#4986](https://github.com/kaldi-asr/kaldi/pull/4986) | @@ -255,7 +255,7 @@ The project name links upstream. | [Quest](https://github.com/mit-han-lab/Quest) ([fork](https://github.com/AMD-Ecosystem/Quest/tree/moat-port)) | ✅ | ✅ | ⬜ | — | | [QUICK](https://github.com/merzlab/QUICK) ([fork](https://github.com/AMD-Ecosystem/QUICK/tree/moat-port)) | ✅ | ✅ | 🚫 | — | | [rmagine](https://github.com/uos/rmagine) ([fork](https://github.com/AMD-Ecosystem/rmagine/tree/moat-port)) | ✅ | ✅ | ✅ | — | -| [rmcl](https://github.com/uos/rmcl) ([fork](https://github.com/AMD-Ecosystem/rmcl/tree/moat-port)) | 🔧 | ⬜ | ⬜ | — | +| [rmcl](https://github.com/uos/rmcl) ([fork](https://github.com/AMD-Ecosystem/rmcl/tree/moat-port)) | 🔧 | 🔧 | ⬜ | — | | [RWKV-CUDA](https://github.com/BlinkDL/RWKV-CUDA) ([fork](https://github.com/AMD-Ecosystem/RWKV-CUDA/tree/moat-port)) | ✅ | ✅ | ✅ | — | | [RXMesh](https://github.com/owensgroup/RXMesh) ([fork](https://github.com/AMD-Ecosystem/RXMesh/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#73](https://github.com/owensgroup/RXMesh/pull/73) | | [SCAMP](https://github.com/zpzim/SCAMP) ([fork](https://github.com/AMD-Ecosystem/SCAMP/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#145](https://github.com/zpzim/SCAMP/pull/145) | diff --git a/VISUAL.md b/VISUAL.md index bc0f9c40..4d729f5c 100644 --- a/VISUAL.md +++ b/VISUAL.md @@ -137,6 +137,12 @@ flowchart TB mp == "the one pull request" ==> updef ``` +Once that pull request is open, `moat-port` freezes: it is the PR's head, so a +push to it is visible to the maintainer immediately. A requested fix is built and +proven on a third branch, `moat-fix-`, and only an approved fix round +fast-forwards `moat-port` -- so the pull request only ever gains commits a person +approved. + --- ## 5. Coverage is gates, not GPUs diff --git a/schema/status.schema.json b/schema/status.schema.json index f337925f..94ca44d6 100644 --- a/schema/status.schema.json +++ b/schema/status.schema.json @@ -273,6 +273,44 @@ "pr_closed_note": { "type": "string" }, + "published_sha": { + "type": [ + "string", + "null" + ] + }, + "fix": { + "type": [ + "object", + "null" + ], + "required": [ + "branch", + "base_sha" + ], + "properties": { + "branch": { + "type": "string", + "minLength": 1 + }, + "base_sha": { + "type": "string", + "minLength": 1 + }, + "review_pr": { + "type": [ + "string", + "null" + ] + }, + "opened_at": { + "type": "string" + } + } + }, + "fix_merged_at": { + "type": "string" + }, "review_pr": { "type": "string" }, diff --git a/utils/check.py b/utils/check.py index 3687e844..4d556522 100644 --- a/utils/check.py +++ b/utils/check.py @@ -278,6 +278,69 @@ def gate_forks(): return [l for l in r.stdout.splitlines() if l.strip()][:10] +def gate_published(): + """No local clone carries commits on an open PR's branch that the PR does not. + + A local branch is not evidence about a remote one -- it is only as fresh as the + last fetch -- so this asks the one question a clone CAN answer: is its copy of + the port branch AHEAD of the published tip? That is the precursor to an + unapproved push, and the fix flow's answer to it is `moat-fix-`. Behind is + just a stale fetch and passes silently, which also keeps `--merge-fix` from + failing this gate on the very host that performed the approved merge: that push + goes to the fork by sha and leaves the local branch where it was. + + Diverged (neither one an ancestor) is reported too -- the branch was rebased or + rewritten under an open PR. Where no clone exists the gate says it judged + nothing; the reconciler (`upstream.py --dry-run`) covers the remote side.""" + sys.path.insert(0, str(REPO / "utils")) + import moatlib + problems, judged = [], 0 + for src in sorted((REPO / "projects").glob("*/src/.git")): + name = src.parent.parent.name + try: + obj, _where = moatlib.project_record(name) + except Exception: + continue + if not obj or obj.get("pr_state") != "open": + continue + judged += 1 + pub = obj.get("published_sha") + if not pub: + # Records from before this field existed backfill on the first + # fix-branch call; until then there is nothing to compare. + print(f"published: note -- {name} has an open PR but no published_sha " + f"recorded (predates the fix flow; `moatlib.py fix-branch {name}` " + f"backfills it)", file=sys.stderr) + continue + branch = obj.get("fork_branch") or moatlib.PORT_BRANCH + clone = str(src.parent) + tip = _run(["git", "-C", clone, "rev-parse", "--verify", "--quiet", + f"refs/heads/{branch}"]).stdout.strip() + if not tip or moatlib.same_commit(tip, pub): + continue # no local copy of the branch, or in step + if _run(["git", "-C", clone, "cat-file", "-e", f"{pub}^{{commit}}"]).returncode: + continue # the published commit was never fetched here + if not _run(["git", "-C", clone, "merge-base", "--is-ancestor", + tip, pub]).returncode: + continue # behind: a stale fetch is not a problem + ahead = not _run(["git", "-C", clone, "merge-base", "--is-ancestor", + pub, tip]).returncode + count = _run(["git", "-C", clone, "rev-list", "--count", + f"{pub}..{tip}"]).stdout.strip() or "?" + problems.append( + f"{name}: local {branch} carries {count} commit(s) the open upstream PR " + f"({pub[:12]}) does not" + + ("" if ahead else ", and is not descended from it -- the branch was " + "rewritten under an open PR") + + f". A fix goes on moat-fix- (`moatlib.py fix-branch {name}`); " + f"only `upstream.py --merge-fix --apply` moves {branch}") + if not problems and judged == 0: + print("published: VACUOUS -- no project in this checkout has both an open " + "PR and a fork clone, so this gate judged nothing here", + file=sys.stderr) + return problems + + def gate_gh_guard(): """The gh guard classifies correctly, and on a working host it is actually wired in. @@ -470,6 +533,7 @@ def gate_harness_adapters(): "jargon": (gate_jargon, False), "optout": (gate_optout, False), "surface": (gate_surface, False), + "published": (gate_published, False), "forks": (gate_forks, True), # slow: shells out per fork clone } diff --git a/utils/gen_schema.py b/utils/gen_schema.py index 25a3682f..c3e0446b 100644 --- a/utils/gen_schema.py +++ b/utils/gen_schema.py @@ -153,6 +153,24 @@ def build(): "pr_merged_at": {"type": "string"}, "pr_closed_at": {"type": "string"}, "pr_closed_note": {"type": "string"}, + # What the open upstream PR shows. While a fix round is staged, + # head_sha runs ahead of this on the staging branch; only the trusted + # merge path (upstream.py --merge-fix) advances it. + "published_sha": {"type": ["string", "null"]}, + # A maintainer-requested fix round in flight: the staging branch cut + # from the published tip, and the fork review PR where a person + # approves the delta. Cleared when the round merges. + "fix": { + "type": ["object", "null"], + "required": ["branch", "base_sha"], + "properties": { + "branch": {"type": "string", "minLength": 1}, + "base_sha": {"type": "string", "minLength": 1}, + "review_pr": {"type": ["string", "null"]}, + "opened_at": {"type": "string"}, + }, + }, + "fix_merged_at": {"type": "string"}, # The review PR on our own fork: where a maintainer sees the code, title # and body together, and approves once. "review_pr": {"type": "string"}, diff --git a/utils/moatlib.py b/utils/moatlib.py index c61f14de..78be2ac7 100755 --- a/utils/moatlib.py +++ b/utils/moatlib.py @@ -1006,7 +1006,10 @@ def record_pr_approval(name, review_pr=None): what they approved so an unattended job can later prove that what it is about to publish upstream is that same thing.""" obj = load_status(name) - url = review_pr or obj.get("review_pr") + # A fix round's approval lives on the fix review PR; the original review_pr + # already did its job when the upstream PR opened. + fix_url = (obj.get("fix") or {}).get("review_pr") + url = review_pr or fix_url or obj.get("review_pr") if not url: raise ValueError(f"{name}: no review_pr recorded; pass --review-pr ") pr = fetch_review_pr(url) @@ -1024,7 +1027,11 @@ def record_pr_approval(name, review_pr=None): review = _approving_review(pr) if review is None: raise ValueError(f"{name}: no standing approval on {url}") - obj["review_pr"] = pr.get("url") or url + resolved = pr.get("url") or url + if fix_url and resolved.rstrip("/") == fix_url.rstrip("/"): + obj["fix"]["review_pr"] = resolved # keep the original review_pr intact + else: + obj["review_pr"] = resolved obj["pr_approval"] = { "approved_by": review.get("login"), "at": review.get("at") or now_iso(), @@ -1033,7 +1040,7 @@ def record_pr_approval(name, review_pr=None): # and the approved one is the truth. "head_sha": review.get("commit") or pr.get("headRefOid"), "content_sha256": _content_digest(pr), - "review_pr": obj["review_pr"], + "review_pr": resolved, } save_record(name, obj, f"{name}: snapshot the approval standing on the review PR\n\n" @@ -1399,6 +1406,10 @@ def set_pr_open(name, pr_url, pr_number): obj["pr_number"] = int(pr_number) obj["pr_opened_at"] = now_iso() obj["pr_state"] = "open" + # What the open PR shows. From here on, head_sha may run ahead of this on a + # staging branch (see fix_branch); the PR branch itself moves only through + # `upstream.py --merge-fix`, which is what advances this field. + obj["published_sha"] = obj.get("head_sha") save_record(name, obj, f"{name}: upstream PR opened -- {obj['pr_url']}") return obj @@ -1432,6 +1443,309 @@ def _fork_repo(name): return PROJECTS / name / "src" +# ---- fix rounds on an open upstream PR ------------------------------------- +# +# Once an upstream PR is open, its head branch is upstream-visible: any push to it +# lands in front of the maintainer immediately, before review, revalidation, or a +# person's approval. So a maintainer-requested fix is staged on `moat-fix-`, +# cut from the published tip, and the pipeline (porter -> reviewer -> validators) +# runs against that staging tip. head_sha follows the staging tip -- it keeps +# meaning "fork port tip under evaluation", so revalidate/pr-gate derivation is +# unchanged -- while `published_sha` records what the open PR shows. The one thing +# that may move the PR branch is `upstream.py --merge-fix --apply`, after a person +# approves the delta on a fork review PR (see set_fix_merged). + +def record_writable_here(name): + """Can this checkout write this project's record at all? (ok, why). + + save_record's own precedence, asked in advance. A trunk-resident record with no + `port/` branch is writable only from a checkout of the protected trunk, so + every fix-round command would raise -- and the merge path would raise AFTER it + had already moved the upstream PR. Asking first turns that into a refusal with a + remedy.""" + obj, where = project_record(name) + if obj is None: + return (False, f"{name}: no record on any ref") + if status_path(name).exists() and writable_here(name, where): + return (True, "in this working tree") + branch = port_branch_of(name) + if branch: + return (True, f"on {branch}") + return (False, + f"{name}: its record is on the trunk and this checkout is on " + f"{current_branch()}, which may not write it. Work in flight belongs on " + f"`port/{name}`: `python3 utils/moatlib.py fix-branch {name}` re-homes " + f"the folder there before its first write") + + +def ensure_port_branch(name): + """Put a trunk-resident project's folder back on `port/`. (moved, message). + + A maintainer asking for a code change makes a finished project unfinished again, + and belongs_on_branch says its folder has to go back -- but that predicate only + flips once head_sha moves, which cannot happen until a fix round is recorded, + which cannot be written while the record sits on the protected trunk. Something + has to break the circle, and the fix round is the event that means "in flight + again", so it breaks it: cut the claim from the trunk, where the folder already + is, and every later write lands on the branch through save_record. + + Not a human decision -- the project was adopted long ago and this creates no + fork, no PR and no upstream contact. Idempotent; a project that already has a + branch is left exactly alone.""" + if port_branch_of(name): + return (False, f"{name}: already on port/{name}") + main_ref = _git("rev-parse", "--verify", "-q", "origin/main", + check=False).stdout.strip() + if not main_ref: + raise ValueError(f"{name}: cannot read origin/main to cut port/{name} from") + if not _ref_read("origin/main", f"projects/{name}/status.json"): + raise ValueError(f"{name}: its record is not on origin/main either -- there " + f"is nothing to re-home; find where it lives first") + r = _git("push", "-q", "origin", f"{main_ref}:refs/heads/port/{name}", check=False) + if r.returncode: + raise ValueError(f"{name}: could not create port/{name}: " + f"{(r.stderr or r.stdout).strip()}") + # Do not wait for a fetch to make the new branch visible: port_branch_of reads + # remote-tracking refs, and save_record is about to ask. + _git("update-ref", f"refs/remotes/origin/port/{name}", main_ref, check=False) + _PORT_BRANCH_MAP.clear() + return (True, f"{name}: re-homed onto port/{name} (cut from origin/main at " + f"{main_ref[:12]}) -- work in flight lives on its own branch") + + +def _verified_published_sha(obj, name): + """What the open PR actually shows right now, for a record written before + published_sha existed. + + The backfilled value becomes the ancestry baseline the merge fast-forward is + checked against AND the baseline --dry-run calls a maintainer push, so it is the + last field in this flow that should be assumed. head_sha is the right guess -- + nothing was supposed to have moved the PR branch -- but the entire point of this + flow is that "supposed to" is not evidence, so ask GitHub and only accept the + guess when it agrees.""" + repo, num = _pr_ref(obj.get("pr_url")) + if not repo: + raise ValueError(f"{name}: no usable pr_url to verify the published tip against") + live = _gh_json("pr", "view", num, "--repo", repo, "--json", "headRefOid") + if not live or not live.get("headRefOid"): + raise ValueError( + f"{name}: cannot read {repo}#{num} to confirm what the open PR shows, and " + f"the published tip is the baseline every later check rests on -- retry " + f"when GitHub is reachable rather than recording a guess") + head = live["headRefOid"] + if not same_commit(head, obj.get("head_sha")): + raise ValueError( + f"{name}: the open PR is at {head[:12]} but the record says head_sha " + f"{(obj.get('head_sha') or '?')[:12]}. The published tip cannot be " + f"inferred from a record that disagrees with the PR -- this is the " + f"HEAD-MOVED case (`upstream.py --dry-run`): read what landed and let a " + f"person rule on it before staging a fix round") + return head + + +def fix_branch(name): + """Establish (or report) the staging branch for a fix round. + + Record-only: the branch itself is the porter's git work; this names it so no + porter invents a name, and pins the base so descent from the published tip can + be checked at merge time. Idempotent for the recorded branch; refuses to open a + second round while one is in flight. A record from before published_sha existed + is backfilled from what the PR actually shows, confirmed against GitHub.""" + obj, _where = project_record(name) + if obj is None: + raise FileNotFoundError(str(status_path(name))) + if obj.get("pr_state") != "open": + raise ValueError(f"{name}: no open upstream PR -- fixes stage only while " + f"one is open; otherwise the port branch is still private " + f"and the porter pushes it directly") + if not obj.get("pr_number"): + raise ValueError(f"{name}: pr_state is open but no pr_number is recorded") + branch = f"moat-fix-{obj['pr_number']}" + fix = obj.get("fix") + if fix: + if fix.get("branch") != branch: + raise ValueError(f"{name}: a fix round is already in flight on " + f"{fix.get('branch')!r} -- one staging branch at a " + f"time; merge or abandon it first") + return fix + # Before the first write, not after it fails: most open-PR records are on the + # trunk, and every command in this flow writes the record. + moved, why = ensure_port_branch(name) + if moved: + print(why, file=sys.stderr) + # Write on top of exactly what the new branch holds, which is the trunk's + # copy -- this checkout's copy of somebody else's folder may be older. + obj, _where = project_record(name) + ok, why = record_writable_here(name) + if not ok: + raise ValueError(why) + if not obj.get("published_sha"): + obj["published_sha"] = _verified_published_sha(obj, name) + obj["fix"] = {"branch": branch, "base_sha": obj["published_sha"], + "review_pr": None, "opened_at": now_iso()} + save_record(name, obj, f"{name}: fix round staged on {branch} " + f"(base {obj['published_sha'][:12]})") + return obj["fix"] + + +def set_fix_review_pr(name, url): + """Record the fork review PR where a person approves the staged delta. + + Mirrors set_review_pr's refusal: recording one asserts the delta is finished, + so every required gate must hold at the staging tip first. Clearing is always + allowed -- undoing a mistake must not require the gates to pass.""" + # project_record, not load_status: the freshest record rather than the nearest. + # A port branch carries a copy of every folder the trunk had when it was cut, so + # reading the working tree here would edit a stale copy and save_record would + # then write it over the branch's real one. + obj, _where = project_record(name) + if obj is None: + raise FileNotFoundError(str(status_path(name))) + if not obj.get("fix"): + raise ValueError(f"{name}: no fix round in flight (moatlib.py fix-branch " + f"establishes one)") + if url: + ready, blocking, _ = fix_ready(name) + if not ready: + listed = ", ".join(f"{p}={s}" for p, s in blocking) + raise ValueError( + f"{name}: cannot record a fix review PR while blocked: {listed}. " + f"The fix review PR is where a person approves the FINISHED delta; " + f"`upstream.py --fix-review --apply --name {name}` opens and " + f"records it once the gates pass.") + obj["fix"]["review_pr"] = url or None + save_record(name, obj, f"{name}: fix review PR " + + (f"recorded -- {url}" if url else "cleared")) + return obj + + +def set_fix_merged(name, new_published_sha): + """The approved staging tip is now what the open PR shows. + + Called by the trusted merge path after the fast-forward push succeeds; the + approval checks live there, not here. Clears the fix block -- the round is + over, and the next one starts from the new published tip.""" + obj, _where = project_record(name) # freshest, not nearest; see above + if obj is None: + raise FileNotFoundError(str(status_path(name))) + fix = obj.get("fix") + if not fix: + raise ValueError(f"{name}: no fix round in flight") + new_published_sha = full_sha(new_published_sha, _fork_repo(name)) + obj["published_sha"] = new_published_sha + obj["fix"] = None + obj["fix_merged_at"] = now_iso() + save_record(name, obj, + f"{name}: fix round merged -- {fix.get('branch')} fast-forwarded " + f"the PR branch to {new_published_sha[:12]}") + return obj + + +def pr_state_of(name, refresh=False): + """(pr_state, where) resolved from wherever the record lives, or (None, why). + + The distinction the fork pre-push hook is built on: None means the state could + not be READ, which is not the same answer as "no PR". `refresh` re-fetches the + project's refs first, because the question is about a write another host made -- + a remote-tracking ref that predates the PR opening would answer "not open" about + a PR that is open.""" + if refresh: + _git("fetch", "--quiet", "origin", + f"+refs/heads/port/{name}:refs/remotes/origin/port/{name}", check=False) + _git("fetch", "--quiet", "origin", + "+refs/heads/main:refs/remotes/origin/main", check=False) + _PORT_BRANCH_MAP.clear() + obj, where = project_record(name) + if obj is None: + return (None, f"no record for {name} on any ref") + return (obj.get("pr_state") or "none", where) + + +# Installed into a fork clone's .git/hooks/pre-push by protect_fork. It asks moatlib +# for the state rather than reading a path, because the record usually is NOT a file +# in the MOAT working tree: an in-flight project's folder lives on its own branch, +# and `projects//status.json` is absent from every checkout standing anywhere +# else. Reading the path directly made the hook exit 0 on exactly the hosts and +# exactly the projects it exists to protect. MOAT_PUBLISH=1 is set only by the +# trusted merge path after its approval checks pass. +FORK_HOOK_MARKER = "# moat-fork-hook" +_FORK_HOOK = """#!/usr/bin/env bash +{marker} v2 +# Refuses pushes to the upstream PR's head branch while that PR is open. +# Installed by `moatlib.py protect-fork {name}`; see AGENTS.md on fix rounds. +set -u +[ "${{MOAT_PUBLISH:-}}" = "1" ] && exit 0 + +# Only the PR's head branch is guarded, so the staging branch and any scratch +# branch push normally -- and the lookup below is paid for only on the one push +# that could reach a maintainer. +targets=0 +while read -r _local _lsha remote _rsha; do + [ "$remote" = "refs/heads/{branch}" ] && targets=1 +done +[ "$targets" = 1 ] || exit 0 + +# stdout only, so nothing a warning or a git message writes to stderr can be read +# as the answer; the exit status is what says whether there IS an answer. +state=$(python3 "{moatlib}" pr-state "{name}" --refresh 2>/dev/null) +if [ $? -ne 0 ]; then + # A guard that cannot tell must not be the reason an unreviewed commit reached a + # maintainer. Refuse, and say what to run to find out why. + echo >&2 "moat: cannot tell whether {name} has an open upstream PR, so this push" + echo >&2 "moat: to {branch} is refused rather than guessed at. Check with:" + echo >&2 "moat: python3 {moatlib} pr-state {name}" + python3 "{moatlib}" pr-state "{name}" 2>&1 >/dev/null | sed >&2 's/^/moat: /' + exit 1 +fi +if [ "$state" = "open" ]; then + echo >&2 "moat: {branch} is the head of an OPEN upstream PR -- a push to it is" + echo >&2 "moat: upstream-visible before anyone reviewed or approved it." + echo >&2 "moat: Stage the fix instead: python3 utils/moatlib.py fix-branch {name}" + echo >&2 "moat: (the approved merge runs through: utils/upstream.py --merge-fix)" + exit 1 +fi +exit 0 +""" + + +def protect_fork(name): + """Install the pre-push hook that keeps an open PR's branch from moving. + + Returns (level, message) where level is "ok", "skip" or "warn"; the CLI sends + "warn" to stderr so an unprotected clone is not indistinguishable from a + protected one in orient's output. Idempotent; refuses to clobber a hook that is + not ours (say so, loudly, rather than silently replacing whatever someone + installed) but does replace an older moat hook. A missing clone installs nothing + and says so -- absence of a clone is absence of the risk.""" + repo = _fork_repo(name) + git_dir = repo / ".git" + if not git_dir.exists(): + return ("skip", f"{name}: no fork clone at {repo} -- nothing to protect") + if not git_dir.is_dir(): + return ("warn", f"{name}: {repo} keeps its git dir elsewhere (a .git file), so " + f"the hook path cannot be derived here -- UNPROTECTED") + obj, _where = project_record(name) + if obj is None: + return ("warn", f"{name}: a fork clone exists but no record does; cannot tell " + f"which branch to protect") + text = _FORK_HOOK.format(marker=FORK_HOOK_MARKER, name=name, + moatlib=str(REPO_ROOT / "utils" / "moatlib.py"), + branch=obj.get("fork_branch") or PORT_BRANCH) + hook = git_dir / "hooks" / "pre-push" + if hook.exists(): + current = hook.read_text() + if current == text: + return ("ok", f"{name}: fork pre-push hook installed and current") + if FORK_HOOK_MARKER not in current: + return ("warn", f"{name}: a NON-moat pre-push hook is installed at {hook}; " + f"not touching it -- {obj.get('fork_branch') or PORT_BRANCH} " + f"is UNPROTECTED in this clone") + hook.parent.mkdir(parents=True, exist_ok=True) + hook.write_text(text) + hook.chmod(hook.stat().st_mode | 0o111) + return ("ok", f"{name}: fork pre-push hook installed") + + # Tracked file kinds whose UNCOMMITTED modification in a fork is the integrity-gap # fingerprint: a validation built against local source/build edits that were never # committed, leaving the branch (and any PR off it) unbuildable. (Motivated by the @@ -3053,6 +3367,14 @@ def pr_ready(name): if obj.get("pr_url"): return (False, [("pr-exists", "a PR is already recorded in status.json")], []) + blocking, nonviable = _gate_blockers(name, obj) + return (not blocking, blocking, nonviable) + + +def _gate_blockers(name, obj): + """The gate core shared by pr_ready and fix_ready: coverage gates, licence, + and fork integrity, judged at the record's current head_sha. Returns + (blocking, nonviable) with blockers deduped.""" vals = validations(obj) blocking, nonviable = [], [] waivers = obj.get("waivers") or {} @@ -3115,7 +3437,43 @@ def pr_ready(name): for item in blocking: if item not in seen: seen.add(item); deduped.append(item) - return (not deduped, deduped, sorted(set(nonviable))) + return (deduped, sorted(set(nonviable))) + + +def fix_ready(name): + """Is a staged fix round ready for its fork review PR and merge? + + The same bar as pr_ready -- every required gate satisfied at the current + head_sha (the staging tip, under a fix round), licence standing, fork clean -- + with the PR-existence check inverted: an OPEN upstream PR is the precondition + here, not a blocker. The opt-out check binds exactly as it does for a first + submission: a fix push is a new arrival in someone's repository. + + Returns (ready, blocking, nonviable) like pr_ready.""" + obj, _where = project_record(name) + if obj is None: + raise FileNotFoundError(str(status_path(name))) + opt = optout_for(upstream_full_name(name) or "") + if opt: + return (False, [("opted-out", + f"{opt['who']} asked not to receive pull requests from this " + f"effort ({opt['source']})")], []) + if obj.get("pr_state") != "open": + return (False, [("no-open-pr", "fix rounds exist only while an upstream PR " + "is open")], []) + fix = obj.get("fix") + if not fix: + return (False, [("no-fix-round", "no staging branch recorded " + "(moatlib.py fix-branch)")], []) + if not obj.get("published_sha"): + return (False, [("no-published-sha", "the record does not say what the open " + "PR shows")], []) + if same_commit(obj.get("head_sha"), obj.get("published_sha")): + return (False, [("no-delta", f"head_sha equals published_sha " + f"({(obj.get('head_sha') or '?')[:12]}) -- " + f"nothing is staged")], []) + blocking, nonviable = _gate_blockers(name, obj) + return (not blocking, blocking, nonviable) def record_tokens(name, tokens, source=None): @@ -3327,6 +3685,39 @@ def main(argv=None): s.add_argument("pr_url") s.add_argument("pr_number", type=int) + s = sub.add_parser("fix-branch", + help="establish or report the staging branch for a fix round " + "on an open upstream PR") + s.add_argument("name") + + s = sub.add_parser("set-fix-review-pr", + help="record the fork review PR where the staged fix delta is approved") + s.add_argument("name") + s.add_argument("url", nargs="?") + s.add_argument("--clear", action="store_true", + help="retract a recorded fix review PR; always allowed") + + s = sub.add_parser("fix-ready", + help="check a staged fix round: every required gate satisfied at the staging tip") + s.add_argument("name") + + s = sub.add_parser("protect-fork", + help="install the fork pre-push hook that refuses pushes to an open PR's branch") + s.add_argument("name", nargs="?", default=None, + help="one project, or omit to protect every local fork clone") + + s = sub.add_parser("set-fix-merged", + help="record a fix round the trusted merge path already pushed " + "(recovery only; it authorises nothing)") + s.add_argument("name") + s.add_argument("new_published_sha") + + s = sub.add_parser("pr-state", + help="a project's recorded upstream PR state, from whichever ref holds it") + s.add_argument("name") + s.add_argument("--refresh", action="store_true", + help="fetch the project's refs first (the fork pre-push hook does)") + s = sub.add_parser("set-pr-merged", help="record that the upstream PR merged") s.add_argument("name") @@ -3562,6 +3953,7 @@ def main(argv=None): elif args.cmd == "pr-commands": obj, _where = project_record(args.name) url = ((obj or {}).get("pr_approval") or {}).get("review_pr") \ + or ((obj or {}).get("fix") or {}).get("review_pr") \ or (obj or {}).get("review_pr") if not url: print(f"{args.name}: no review PR recorded") @@ -3652,6 +4044,61 @@ def main(argv=None): elif args.cmd == "set-pr-open": set_pr_open(args.name, args.pr_url, args.pr_number) print(f"{args.name}: PR opened -> {args.pr_url}") + elif args.cmd == "fix-branch": + fix = fix_branch(args.name) + print(f"{args.name}: fix round on {fix['branch']} " + f"(base {(fix.get('base_sha') or '?')[:12]}, review PR " + f"{fix.get('review_pr') or 'not yet open'})") + elif args.cmd == "set-fix-review-pr": + # Same refusal shape as set-review-pr: the bare form must not retract. + if not args.url and not args.clear: + try: + cur = ((load_status(args.name).get("fix") or {}).get("review_pr") + or "none recorded") + except (FileNotFoundError, ValueError) as e: + cur = f"unreadable -- {e}" + print(f"set-fix-review-pr: no URL given. Pass one to record it, or " + f"--clear to retract the recorded one. {args.name} currently: " + f"{cur}", file=sys.stderr) + return 2 + set_fix_review_pr(args.name, None if args.clear else args.url) + print(f"{args.name}: fix review PR -> " + f"{'(cleared)' if args.clear else args.url}") + elif args.cmd == "fix-ready": + ready, blocking, nonviable = fix_ready(args.name) + print(f"{args.name}: fix-ready={ready}") + if blocking: + print(" BLOCKING (every required gate needs ONE completed arch at the " + "staging tip, or an approved waiver; fork must be clean): " + + ", ".join(f"{p}={s}" for p, s in blocking)) + if nonviable: + print(" non-viable (does not block; scope the claim): " + + ", ".join(nonviable)) + return 0 if ready else 1 + elif args.cmd == "protect-fork": + names = [args.name] if args.name else sorted( + p.parent.name for p in PROJECTS.glob("*/src/.git")) + if not names: + print("protect-fork: no fork clones in this checkout") + warned = 0 + for n in names: + level, message = protect_fork(n) + # orient.sh discards stdout; a clone it could NOT protect has to be the + # one thing that still reaches the operator. + print(message, file=sys.stderr if level == "warn" else sys.stdout) + warned += level == "warn" + if warned: + print(f"protect-fork: {warned} fork clone(s) are UNPROTECTED (above)", + file=sys.stderr) + elif args.cmd == "set-fix-merged": + obj = set_fix_merged(args.name, args.new_published_sha) + print(f"{args.name}: published_sha -> {obj['published_sha'][:12]}") + elif args.cmd == "pr-state": + state, why = pr_state_of(args.name, refresh=args.refresh) + if state is None: + print(why, file=sys.stderr) + return 1 + print(state) elif args.cmd == "set-pr-merged": set_pr_merged(args.name) print(f"{args.name}: PR merged") diff --git a/utils/orient.sh b/utils/orient.sh index c9c9abbc..a91a1e47 100755 --- a/utils/orient.sh +++ b/utils/orient.sh @@ -9,6 +9,11 @@ cd "$(dirname "$0")/.." bash utils/setup_git.sh >/dev/null 2>&1 || true python3 utils/install_hooks.py >/dev/null 2>&1 || true +# Fork clones get their own pre-push hook: while a project's upstream PR is open, +# its port branch may not move except through `upstream.py --merge-fix`. Only stdout +# is discarded: a clone this could NOT protect reports on stderr, and an unprotected +# clone that looks exactly like a protected one is the failure this guards against. +python3 utils/moatlib.py protect-fork >/dev/null || true # Refuse to work on the trunk. Project state belongs on a shared port/ branch, # and that branch existing on the remote is what tells another host the project is diff --git a/utils/upstream.py b/utils/upstream.py index 6f894a0c..25caa404 100644 --- a/utils/upstream.py +++ b/utils/upstream.py @@ -27,6 +27,9 @@ python3 utils/upstream.py --review # report ports needing a review PR python3 utils/upstream.py --publish # report approved ports ready to submit python3 utils/upstream.py --publish --apply # open the upstream PRs + python3 utils/upstream.py --fix-review # staged fix rounds needing a review PR + python3 utils/upstream.py --merge-fix # approved fix rounds ready to merge + python3 utils/upstream.py --merge-fix --apply # fast-forward the open PR to the approved tip Record maintenance and one publishing step. None of it does any porting -- that needs a GPU host and a session. These keep the record true, tell someone, and send an approved @@ -39,6 +42,7 @@ import argparse import json +import os import pathlib import re import subprocess @@ -149,16 +153,25 @@ def recorded(): continue ours = d.get("pr_state") or ("merged" if d.get("pr_merged_at") else None) out.append({"name": name, "repo": m.group(1), "num": m.group(2), - "url": pr, "ours": ours}) + "url": pr, "ours": ours, "published": d.get("published_sha")}) return out def poll(rows): - """Compare each record against GitHub. Returns (drift, unreviewed, errors).""" - drift, unreviewed, errors = [], [], [] + """Compare each record against GitHub. + + Returns (drift, unreviewed, headdrift, errors). headdrift is an OPEN PR whose + head no longer matches the recorded published_sha: a push that did not come + through the fix flow's merge. The usual cause is a maintainer editing our + branch (allowed on PRs with maintainer-edit enabled), which was done on + purpose by a person -- so it is never auto-absorbed: review the commit(s) and + recommend a course of action for a human to decide.""" + sys.path.insert(0, str(REPO / "utils")) + import moatlib + drift, unreviewed, headdrift, errors = [], [], [], [] for r in rows: d = gh_json(["pr", "view", r["num"], "--repo", r["repo"], "--json", - "state,mergedAt,reviewDecision,updatedAt"]) + "state,mergedAt,reviewDecision,updatedAt,headRefOid"]) if d is None: errors.append({**r, "why": "lookup failed"}) continue @@ -174,7 +187,14 @@ def poll(rows): # An open PR with changes requested is work waiting on us, not drift. if r["real"] == "OPEN" and r["review"] == "CHANGES_REQUESTED": unreviewed.append(r) - return drift, unreviewed, errors + # same_commit, not `!=`: recorded shas arrive at whatever length whoever + # wrote them used, and an abbreviated published_sha compared literally + # against a 40-character headRefOid would report a maintainer push on every + # sweep. Five validated_shas already read as stale forever that way. + if (r["real"] == "OPEN" and r.get("published") and d.get("headRefOid") + and not moatlib.same_commit(d["headRefOid"], r["published"])): + headdrift.append({**r, "head": d["headRefOid"]}) + return drift, unreviewed, headdrift, errors OURS = {"jeffdaily"} # accounts that speak for this project @@ -803,6 +823,462 @@ def moatlib_record(name): return moatlib.record_pr_approval(name) +# ---- fix rounds: review and merge ------------------------------------------ +# +# While an upstream PR is open its head branch is upstream-visible, so fixes stage +# on `moat-fix-` (moatlib.fix_branch) and reach the PR only here: a person +# approves the delta on a fork review PR, and --merge-fix fast-forwards the PR +# branch to exactly the approved tip. The same contract as --publish: the one +# write is mechanical because a person approved exactly that content, and every +# check re-runs live at merge time. + +# The fix review PR's body section that becomes the upstream reply, posted +# verbatim as a comment on the upstream PR after the merge. Approving the PR +# approves it along with the code. No section means no comment is posted. +REPLY_HEADING = "## Upstream reply" + + +def fix_reply_of(body): + """The reply text a fix review PR's body carries, or None. + + The section ENDS at the next heading of the same level or higher, not at the end + of the body. Everything it contains is posted verbatim in a stranger's + repository, so a `## Notes` written for our own eyes after the reply must not + travel with it. Fenced blocks are skipped when looking for that heading, the + same way jargon.scan_text skips them: a reply that quotes a shell comment is + quoting, not starting a new section.""" + out, collecting, in_fence = [], False, False + for line in (body or "").splitlines(): + stripped = line.strip() + if collecting: + if stripped.startswith("```"): + in_fence = not in_fence + elif not in_fence and re.match(r"^#{1,2}\s+\S", stripped): + break + out.append(line) + elif stripped == REPLY_HEADING: + collecting = True + return "\n".join(out).strip() if collecting else None + + +def _delta_hits_local(clone, base, head, terms, allow): + """The delta scan done in a clone that has both commits, or None if it does not. + + Preferred over the API because it reads the WHOLE delta: `compare` caps its + answer, and this scan is the last thing between in-house vocabulary and a + stranger's repository.""" + sys.path.insert(0, str(REPO / "utils")) + import jargon + if not (clone / ".git").exists(): + return None + for rev in (base, head): + if subprocess.run(["git", "-C", str(clone), "cat-file", "-e", f"{rev}^{{commit}}"], + capture_output=True, text=True).returncode: + return None + msgs = subprocess.run(["git", "-C", str(clone), "log", "--format=%B", + f"{base}..{head}"], capture_output=True, text=True) + diff = subprocess.run(["git", "-C", str(clone), "diff", f"{base}...{head}"], + capture_output=True, text=True) + if msgs.returncode or diff.returncode: + return None + hits = jargon.scan_text(msgs.stdout, "delta commit", terms, allow) + added = "\n".join(l[1:] for l in diff.stdout.splitlines() + if l.startswith("+") and not l.startswith("+++")) + return hits + jargon.scan_text(added, "delta added lines", terms, allow) + + +def _fix_delta_hits(fork, base, head, clone=None): + """Jargon hits in the staged delta's commit messages and added lines. + + From a local clone when one holds both commits, else from the fork over the API + so a host without a clone can still run the check. Raises ValueError when the + comparison cannot be read OR came back truncated -- `compare` caps at 250 + commits and 300 files and omits `patch` on large ones, and a partial answer that + reads like a clean one is worse than no answer. A gate that cannot run is not a + gate that passed.""" + sys.path.insert(0, str(REPO / "utils")) + import jargon + terms, allow = jargon.load() + if clone is not None: + local = _delta_hits_local(clone, base, head, terms, allow) + if local is not None: + return local + cmp = gh_json(["api", f"repos/{fork}/compare/{base}...{head}", + "--jq", "{total: .total_commits, " + "commits: [.commits[].commit.message], " + "files: ((.files // []) | length), " + "nopatch: ([(.files // [])[] | select(.patch == null)] " + "| length), " + "patches: [(.files // [])[].patch // \"\"]}"]) + if cmp is None: + raise ValueError(f"cannot read {fork} compare {base[:12]}...{head[:12]}") + commits = cmp.get("commits") or [] + total = cmp.get("total") + if total is not None and len(commits) < total: + raise ValueError(f"{fork} compare returned {len(commits)} of {total} commits " + f"-- too large to scan over the API; run this from a host " + f"with the fork clone") + if cmp.get("files", 0) >= 300 or cmp.get("nopatch"): + raise ValueError(f"{fork} compare returned a truncated file list " + f"({cmp.get('files')} files, {cmp.get('nopatch')} without a " + f"patch) -- run this from a host with the fork clone") + hits = [] + for msg in commits: + hits += jargon.scan_text(msg, "delta commit", terms, allow) + added = "\n".join(l[1:] for p in (cmp.get("patches") or []) + for l in p.splitlines() + if l.startswith("+") and not l.startswith("+++")) + hits += jargon.scan_text(added, "delta added lines", terms, allow) + return hits + + +def fix_review_rows(): + """Staged fix rounds whose gates are met and which have no review PR yet.""" + sys.path.insert(0, str(REPO / "utils")) + import moatlib + out = [] + for name, d, _where in all_records(): + fix = d.get("fix") + if not fix or fix.get("review_pr") or d.get("pr_state") != "open": + continue + fork = (d.get("fork_url") or "").replace("https://github.com/", "") + if not fork: + continue + ready, blocking, _ = moatlib.fix_ready(name) + if not ready: + out.append({"name": name, "fork": fork, "fix": fix, + "problem": "not fix-ready: " + + ", ".join(f"{p}={s}" for p, s in blocking)}) + continue + # Opening the PR writes the record. Finding out here beats finding out + # after GitHub already has the PR and nothing points at it. + writable, why = moatlib.record_writable_here(name) + if not writable: + out.append({"name": name, "fork": fork, "fix": fix, "problem": why}) + continue + base = fix.get("base_sha") + tip = gh_json(["api", f"repos/{fork}/git/ref/heads/{fix['branch']}", + "--jq", "{sha: .object.sha}"]) + if not tip or not tip.get("sha"): + out.append({"name": name, "fork": fork, "fix": fix, + "problem": f"{fix['branch']} does not exist on the fork"}) + continue + out.append({"name": name, "fork": fork, "fix": fix, "base": base, + "tip": tip["sha"], "problem": None, + "branch": fix["branch"], + "target": d.get("fork_branch") or moatlib.PORT_BRANCH}) + return out + + +def open_fix_review_pr(row, title, body, apply=False): + """Open the fork review PR for a staged fix delta. + + Unlike open_review_pr, the title and body are NOT republished upstream -- the + upstream-visible content is the delta's commits, scanned here, plus the + optional reply section, scanned as the upstream prose it is about to become.""" + sys.path.insert(0, str(REPO / "utils")) + import moatlib + import jargon + import prose + + if row.get("problem"): + return ("blocked", row["problem"]) + try: + hits = _fix_delta_hits(row["fork"], row["base"], row["tip"], + clone=REPO / "projects" / row["name"] / "src") + except ValueError as e: + return ("jargon", f"cannot check the delta for in-house vocabulary: {e}") + if hits: + return ("jargon", "in-house vocabulary in the staged delta: " + + ", ".join(sorted({h[2] for h in hits}))) + reply = fix_reply_of(body) + if REPLY_HEADING in (body or "") and not reply: + return ("reply", f"the body carries {REPLY_HEADING!r} with nothing under " + f"it -- drop the heading or write the reply") + if reply: + terms, allow = jargon.load() + rhits = jargon.scan_text(reply, "upstream reply", terms, allow) + if rhits: + return ("jargon", "in-house vocabulary in the upstream reply: " + + ", ".join(sorted({h[2] for h in rhits}))) + wrapped = prose.check(reply, "upstream reply") + if wrapped: + return ("wrapped", wrapped[0]) + if not apply: + return ("would-open", + f"{row['fork']}: {row['branch']} -> {row['target']} " + f"(base {row['base'][:12]}, tip {row['tip'][:12]}" + + (", carries an upstream reply" if reply else "") + + f")\n\n{title}\n\n{body}") + r = subprocess.run(["gh", "pr", "create", "--repo", row["fork"], + "--head", row["branch"], "--base", row["target"], + "--title", title, "--body", body], + capture_output=True, text=True, timeout=90) + if r.returncode: + return ("error", (r.stderr or r.stdout).strip()) + url = r.stdout.strip().splitlines()[-1] + try: + moatlib.set_fix_review_pr(row["name"], url) + except Exception as e: # noqa: BLE001 - reported, not raised + # The PR exists on GitHub now. Raising here would lose the URL and leave the + # round still reading as "needs a review PR", which opens a second one. + return ("unrecorded", + f"opened {url} but could NOT record it: {e}\n" + f" record it by hand: python3 utils/moatlib.py set-fix-review-pr " + f"{row['name']} {url}") + subprocess.run( + ["gh", "pr", "comment", url, "--body", + f"To approve this fix round, leave a comment containing this line by " + f"itself:\n\n```\n{moatlib.APPROVE_COMMAND}\n```\n\n" + f"To send it back to the porter instead:\n\n" + f"```\n{moatlib.CHANGES_COMMAND}\n```\n\n" + f"Approving covers the commits on this branch" + + (f" and the section under {REPLY_HEADING!r} in the body, which is " + f"posted verbatim as a comment on the upstream pull request after " + f"the merge" if reply else "") + + ". `utils/upstream.py --merge-fix --apply` then fast-forwards the " + "open upstream PR's branch to exactly the approved tip. Anything " + "pushed afterwards, or any edit to the body, voids the approval and " + "needs a fresh one."], + capture_output=True, text=True, timeout=90) + return ("opened", url) + + +def merge_fix_rows(): + """Fix rounds with a recorded review PR, ready for the merge gate.""" + for name, d, _where in all_records(): + fix = d.get("fix") + if fix and fix.get("review_pr") and d.get("pr_state") == "open": + yield name, d, fix + + +def merge_fix_blockers(name, d, fix, pr): + """Everything that must hold before the PR branch moves, re-checked live.""" + sys.path.insert(0, str(REPO / "utils")) + import moatlib + import prose + import jargon + + bad = [] + code, why = moatlib.approval_currency(pr) + if code != "ok": + bad.append(f"approval {code}: {why}") + blockers, _notes = moatlib.moat_command_audit(pr) + bad += blockers + ready, blocking, _ = moatlib.fix_ready(name) + if not ready: + bad.append("not fix-ready: " + ", ".join(f"{p}={s}" for p, s in blocking)) + # The merge ends in a record write. Discovering it cannot happen AFTER the open + # PR has already moved leaves the worst state this flow can produce: the PR + # advanced, published_sha naming the old tip, and the reconciler calling our own + # approved merge a maintainer push. + writable, why = moatlib.record_writable_here(name) + if not writable: + bad.append(why) + + fork = (d.get("fork_url") or "").replace("https://github.com/", "") + tip = pr.get("headRefOid") + pub = d.get("published_sha") + if not tip: + bad.append("cannot read the approved tip from the fix review PR") + if tip and not moatlib.same_commit(tip, d.get("head_sha")): + bad.append(f"the fix review PR's head {tip[:12]} is not the recorded " + f"head_sha {(d.get('head_sha') or '?')[:12]} -- the record and " + f"the approval describe different commits") + reply = fix_reply_of(pr.get("body")) + if REPLY_HEADING in (pr.get("body") or "") and not reply: + bad.append(f"the body carries {REPLY_HEADING!r} with nothing under it") + if reply: + terms, allow = jargon.load() + rhits = jargon.scan_text(reply, "upstream reply", terms, allow) + if rhits: + bad.append("in-house vocabulary in the upstream reply: " + + ", ".join(sorted({h[2] for h in rhits})[:4])) + bad += prose.check(reply, "upstream reply") + + # The merge is a git push, so it needs the clone and needs it to agree with + # GitHub about what is being fast-forwarded from where. + clone = REPO / "projects" / name / "src" + if not (clone / ".git").exists(): + bad.append(f"no fork clone at {clone} -- the merge push runs from a host " + f"that has one") + return bad + fork_url = d.get("fork_url") + branch = d.get("fork_branch") or "moat-port" + ls = subprocess.run(["git", "-C", str(clone), "ls-remote", fork_url, + f"refs/heads/{branch}"], + capture_output=True, text=True, timeout=60) + remote_tip = (ls.stdout.split() or [""])[0] + if ls.returncode or not remote_tip: + bad.append(f"cannot read {branch} on the fork ({fork_url})") + elif not pub or not moatlib.same_commit(remote_tip, pub): + bad.append(f"the fork's {branch} is at {remote_tip[:12]}, not the " + f"published {(pub or '?')[:12]} -- the PR branch moved outside " + f"the fix flow; a person sorts that out first") + if tip: + f = subprocess.run(["git", "-C", str(clone), "fetch", fork_url, + f"+refs/heads/{fix['branch']}:refs/moat/fix"], + capture_output=True, text=True, timeout=120) + have = subprocess.run(["git", "-C", str(clone), "rev-parse", "--verify", + "--quiet", "refs/moat/fix"], + capture_output=True, text=True) + if f.returncode or not moatlib.same_commit(have.stdout.strip(), tip): + bad.append(f"the fork's {fix['branch']} tip does not match the " + f"approved {tip[:12]} -- fetch failed or the branch moved") + elif pub: + anc = subprocess.run(["git", "-C", str(clone), "merge-base", + "--is-ancestor", pub, tip], + capture_output=True, text=True) + if anc.returncode: + bad.append(f"{fix['branch']} is not a descendant of the published " + f"{pub[:12]} -- the staging branch was rebased; the " + f"merge must be a fast-forward") + + # Last, so the fetch above has put the delta in the clone and the scan reads all + # of it rather than whatever the compare API is willing to return. + try: + hits = _fix_delta_hits(fork, fix["base_sha"], tip or fix["branch"], + clone=clone) + if hits: + bad.append("in-house vocabulary in the staged delta: " + + ", ".join(sorted({h[2] for h in hits})[:4])) + except ValueError as e: + bad.append(str(e)) + return bad + + +def _sync_local_port_branch(clone, branch, tip): + """Bring the clone's own copy of the port branch to the tip just published. + + The push above sends a SHA to the fork, which moves nothing locally, so without + this the clone that performed the merge is the one host whose port branch + disagrees with the open PR -- and it fails its own `published` gate for it, on + every push, until someone works out why. Best effort and never fatal: the + upstream write has already happened and a local ref cannot un-happen it.""" + cur = subprocess.run(["git", "-C", str(clone), "symbolic-ref", "--quiet", + "--short", "HEAD"], capture_output=True, text=True) + if cur.stdout.strip() == branch: + # Checked out: only git may move it, and only with a clean tree. + r = subprocess.run(["git", "-C", str(clone), "merge", "--ff-only", tip], + capture_output=True, text=True, timeout=120) + if r.returncode: + return (f"the clone's {branch} still points at the old tip " + f"({(r.stderr or r.stdout).strip()[:80]}); `git -C {clone} " + f"merge --ff-only {tip[:12]}` once the tree is clean") + return None + r = subprocess.run(["git", "-C", str(clone), "update-ref", + f"refs/heads/{branch}", tip], capture_output=True, text=True) + return (f"could not fast-forward the clone's {branch}: " + f"{(r.stderr or r.stdout).strip()[:80]}") if r.returncode else None + + +def do_merge_fix(name, d, fix, pr): + """The pre-authorized write: fast-forward the open PR's branch to the approved + tip, post the approved reply (if the body carries one), record it, and delete the + staging branch. Every check has already run in merge_fix_blockers; like + open_upstream, trusted code proves its own case. + + Recording comes BEFORE the branch deletion. Everything after the push is + best-effort, and the ordering decides what a failure leaves behind: record first + and a failed delete leaves a spare branch anyone can remove, while deleting first + and failing to record leaves a moved PR, a published_sha naming the old tip, and + a fix block pointing at a branch that no longer exists -- which the reconciler + then reports as a maintainer push.""" + sys.path.insert(0, str(REPO / "utils")) + import moatlib + + clone = REPO / "projects" / name / "src" + fork_url = d.get("fork_url") + branch = d.get("fork_branch") or "moat-port" + tip = pr["headRefOid"] + env = {**os.environ, "MOAT_PUBLISH": "1"} + push = subprocess.run(["git", "-C", str(clone), "push", fork_url, + f"{tip}:refs/heads/{branch}"], + capture_output=True, text=True, timeout=120, env=env) + if push.returncode: + return (False, f"push failed: {(push.stderr or push.stdout).strip()[:200]}") + + notes = [] + reply = fix_reply_of(pr.get("body")) + if reply: + import gh_guard + real = gh_guard.real_gh() + if real is None: + notes.append("gh is not installed; the approved reply was NOT posted") + else: + c = subprocess.run([real, "pr", "comment", d["pr_url"], + "--body", reply], + capture_output=True, text=True, timeout=90) + notes.append("posted the approved reply" if c.returncode == 0 else + f"could NOT post the approved reply: " + f"{(c.stderr or c.stdout).strip()[:160]}") + + try: + moatlib.set_fix_merged(name, tip) + except Exception as e: # noqa: BLE001 - reported, not raised + notes.append(f"could NOT record it: {e}; the staging branch is kept so the " + f"round can be recorded by hand " + f"(`moatlib.py set-fix-merged {name} {tip}`)") + return (True, f"merged to {tip[:12]} but " + "; ".join(notes)) + + stale = _sync_local_port_branch(clone, branch, tip) + if stale: + notes.append(stale) + # The branch's job is done and its commits are on the PR branch; a person + # ruled that staging branches are deleted on merge so the next round can + # reuse the name. + rm = subprocess.run(["git", "-C", str(clone), "push", fork_url, + f":refs/heads/{fix['branch']}"], + capture_output=True, text=True, timeout=60, env=env) + if rm.returncode: + notes.append(f"the staging branch {fix['branch']} is still on the fork " + f"and can be deleted by hand") + return (True, f"fast-forwarded {branch} to {tip[:12]}" + + ("; " + "; ".join(notes) if notes else "")) + + +def report_merge_fix(apply, only=None): + rows = [(n, d, f) for n, d, f in merge_fix_rows() if only in (None, n)] + print(f"upstream: {len(rows)} fix round(s) with a review PR recorded\n") + ret = 0 + for name, d, fix in rows: + sys.path.insert(0, str(REPO / "utils")) + import moatlib + pr = moatlib.fetch_review_pr(fix["review_pr"]) + if pr is None: + print(f" UNREACHABLE {name:25} could not read {fix['review_pr']} -- " + f"an outage is not a withdrawn approval") + ret = 1 + continue + bad = merge_fix_blockers(name, d, fix, pr) + if bad: + print(f" HELD {name:26} {bad[0][:78]}") + for b in bad[1:]: + print(f" {'':26} {b[:78]}") + continue + reply = fix_reply_of(pr.get("body")) + print(f" READY {name:26} {fix['branch']} -> " + f"{d.get('fork_branch') or 'moat-port'} at " + f"{(pr.get('headRefOid') or '?')[:12]}" + + (" (+ upstream reply)" if reply else "")) + if not apply: + continue + try: + moatlib_record(name) # who authorised this, before anything moves + except Exception as e: # noqa: BLE001 - reported, not raised + print(f" FAILED to record the approval for {name}: {e}") + ret = 1 + continue + ok, detail = do_merge_fix(name, d, fix, pr) + print(f" {'MERGED' if ok else 'FAILED':10} {name:26} {detail}") + if not ok: + ret = 1 + if rows and not apply: + print("\n --merge-fix --apply performs the fast-forward push (and posts " + "the approved reply, where the body carries one).") + return ret + + RECONCILED = REPO / "data" / "reconciled.json" @@ -846,6 +1322,10 @@ def main(): help="find port approvals overtaken by a later push or edit") ap.add_argument("--publish", action="store_true", help="submit approved ports upstream with their approved title and body") + ap.add_argument("--fix-review", action="store_true", + help="staged fix rounds whose gates are met with no review PR open yet") + ap.add_argument("--merge-fix", action="store_true", + help="fast-forward an open upstream PR to an approved fix round's tip") ap.add_argument("--attention", action="store_true", help="open upstream PRs where a maintainer is waiting on us") a = ap.parse_args() @@ -884,11 +1364,42 @@ def main(): return 0 if action in ("opened", "would-open") else 1 if a.publish: return report_publish(apply=a.apply) + if a.fix_review: + rows = fix_review_rows() + if not a.apply and not a.name: + for r in rows: + if r["problem"]: + print(f" BLOCKED {r['name']:22} {r['problem']}") + else: + print(f" READY {r['name']:22} {r['branch']} -> {r['target']} " + f"(base {r['base'][:12]}, tip {r['tip'][:12]})") + print(f"-- {sum(1 for r in rows if not r['problem'])} fix round(s) need " + f"a review PR; {sum(1 for r in rows if r['problem'])} blocked") + print(" open one: --fix-review --apply --name

--title '' " + "--body-file ") + print(f" (a body section headed {REPLY_HEADING!r} is posted verbatim " + f"on the upstream PR after the merge)") + return 0 + if not (a.name and a.title and a.body_file): + print("--fix-review --name needs --title and --body-file " + "(add --apply to open the PR rather than preview it)", + file=sys.stderr) + return 2 + row = next((r for r in rows if r["name"] == a.name), None) + if row is None: + print(f"{a.name} has no fix round awaiting a review PR", file=sys.stderr) + return 2 + body = pathlib.Path(a.body_file).read_text() + action, detail = open_fix_review_pr(row, a.title, body, apply=a.apply) + print(f"fix-review-pr: {action} -- {detail}") + return 0 if action in ("opened", "would-open") else 1 + if a.merge_fix: + return report_merge_fix(apply=a.apply, only=a.name) if a.attention: return report_attention(recorded(), TODAY) rows = recorded() - drift, unreviewed, errors = poll(rows) + drift, unreviewed, headdrift, errors = poll(rows) skipped = [r for r in rows if r.get("skipped")] # The sweep just happened, so stamp it whether or not anything is applied. # Nothing runs on a schedule any more, so this timestamp is the only thing that @@ -897,14 +1408,27 @@ def main(): stamp_reconciled(len(rows), len(drift)) print(f"upstream: {len(rows)} recorded PRs, {len(drift)} drifted, " - f"{len(unreviewed)} awaiting our response, {len(skipped)} skipped, " - f"{len(errors)} lookup errors\n") + f"{len(unreviewed)} awaiting our response, {len(headdrift)} head-moved, " + f"{len(skipped)} skipped, {len(errors)} lookup errors\n") for r in drift: print(f" DRIFT {r['name']:26} we say {str(r['ours']):16} " f"GitHub says {r['real']:8} {r['repo']}#{r['num']}") for r in unreviewed: print(f" CHANGES {r['name']:26} maintainer requested changes " f"{r['repo']}#{r['num']}") + for r in headdrift: + print(f" HEAD-MOVED {r['name']:26} PR head {r['head'][:12]} != published " + f"{r['published'][:12]} {r['repo']}#{r['num']}") + if headdrift: + # Deliberately never applied: the usual cause is a maintainer pushing to + # our branch, which a person did on purpose. The move is to READ what + # landed and put a recommendation in front of a human, not to absorb or + # revert it. + print("\n a moved head on an open PR is a push outside the fix flow -- " + "usually a maintainer edit. Review the commit(s) between the two " + "shas and recommend a course of action; a person decides. If the " + "content is accepted, a fresh fix round from the new tip re-enters " + "the flow.") for r in skipped: print(f" SKIPPED {r['name']:26} {r['skipped']}") for r in errors: From ac4a1f44abf8bedde21572c2a4d30672445f4417 Mon Sep 17 00:00:00 2001 From: Jeff Daily Date: Wed, 12 Aug 2026 13:30:37 -0700 Subject: [PATCH 4/6] Let a platform that has recorded nothing be blocked The gfx1151 host is back in service, so the selector offered kaldi's windows gate to it: `next-task windows-gfx1151` returned kaldi/port-ready/validator. That is a build that cannot succeed. kaldi's ROCm path is gated on the host OS, not on the GPU -- src/configure's configure_rocm() exits 1 unless uname is Linux and uname -m is x86_64, before any arch is consulted -- and windows-gfx1101 and windows-gfx1201 already say so in their blocked_reason. The gfx1151 record that said it too was removed with the other 132 records of an intention to validate on a machine we no longer had. Removing it was right; an absent record means "no host has worked this platform" and the selector defaults it correctly. But the arch that has recorded nothing is exactly the arch most likely to discover it cannot run the project at all, and set_blocked indexed straight into obj["platforms"][platform] and raised KeyError. The one way to record the finding was the one case it could not handle. So blocking creates the row, the way a stage transition already does. Clearing does not: writing a row that says "not blocked" would put back an intention to validate somewhere, which is fleet state, so --clear on an arch with no record says there is nothing to clear rather than inventing one to unset. kaldi's windows-gfx1151 block is recorded with the configure line that proves it, verified against the fork rather than copied from the sibling archs. The README regeneration that follows also picks up colmap and HEonGPU moving from validated to revalidate, which their advanced heads already implied. Test Plan: ``` python3 utils/moatlib.py set-blocked kaldi windows-gfx1151 "..." # was KeyError python3 utils/moatlib.py set-blocked kaldi linux-gfx942 --clear # nothing to clear python3 utils/moatlib.py validate kaldi python3 utils/moatlib.py next-task windows-gfx1151 # no longer kaldi python3 utils/check.py ``` Authored with Claude (Opus 5) as the AI assistant. --- README.md | 4 ++-- projects/kaldi/status.json | 24 +++++++++++++++++++++++- utils/moatlib.py | 10 ++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 87ca56c6..5f5b0a16 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ The project name links upstream. | [bellhopcuda](https://github.com/A-New-BellHope/bellhopcuda) ([fork](https://github.com/AMD-Ecosystem/bellhopcuda/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#65](https://github.com/A-New-BellHope/bellhopcuda/pull/65) | | [brian2cuda](https://github.com/brian-team/brian2cuda) ([fork](https://github.com/AMD-Ecosystem/brian2cuda/tree/moat-port)) | ✅ | ✅ | 🔄 | 🟢 [#327](https://github.com/brian-team/brian2cuda/pull/327) | | [catboost](https://github.com/catboost/catboost) ([fork](https://github.com/AMD-Ecosystem/catboost-moat/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#3111](https://github.com/catboost/catboost/pull/3111) | -| [colmap](https://github.com/colmap/colmap) ([fork](https://github.com/AMD-Ecosystem/colmap/tree/moat-port)) | ✅ | ✅ | 🎫 | 🟢 [#4635](https://github.com/colmap/colmap/pull/4635) | +| [colmap](https://github.com/colmap/colmap) ([fork](https://github.com/AMD-Ecosystem/colmap/tree/moat-port)) | 🔄 | 🔄 | 🎫 | 🟢 [#4635](https://github.com/colmap/colmap/pull/4635) | | [CPM.cu](https://github.com/OpenBMB/CPM.cu) | 🚫 | — | — | ⚪ not-portable | | [CubbyFlow](https://github.com/utilForever/CubbyFlow) ([fork](https://github.com/AMD-Ecosystem/CubbyFlow/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#145](https://github.com/utilForever/CubbyFlow/pull/145) | | [cuBQL](https://github.com/NVIDIA/cuBQL) ([fork](https://github.com/AMD-Ecosystem/cuBQL/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#35](https://github.com/NVIDIA/cuBQL/pull/35) | @@ -212,7 +212,7 @@ The project name links upstream. | [gRASPA](https://github.com/snurr-group/gRASPA) ([fork](https://github.com/AMD-Ecosystem/gRASPA/tree/moat-port)) | ✅ | ✅ | ✅ | — | | [gtsam_points](https://github.com/koide3/gtsam_points) ([fork](https://github.com/AMD-Ecosystem/gtsam_points/tree/moat-port)) | ✅ | ✅ | ✅ | 🟢 [#99](https://github.com/koide3/gtsam_points/pull/99) | | [h2o4gpu](https://github.com/h2oai/h2o4gpu) | ⬜ | ⬜ | ⬜ | — | -| [HEonGPU](https://github.com/Alisah-Ozcan/HEonGPU) ([fork](https://github.com/AMD-Ecosystem/HEonGPU/tree/moat-port)) | ✅ | ✅ | ⬜ | — | +| [HEonGPU](https://github.com/Alisah-Ozcan/HEonGPU) ([fork](https://github.com/AMD-Ecosystem/HEonGPU/tree/moat-port)) | 🔄 | 🔄 | ⬜ | — | | [icicle](https://github.com/ingonyama-zk/icicle) ([fork](https://github.com/AMD-Ecosystem/icicle/tree/moat-port)) | ✅ | ✅ | 🚫 | — | | [k2](https://github.com/k2-fsa/k2) ([fork](https://github.com/AMD-Ecosystem/k2/tree/moat-port)) | ✅ | ✅ | ✅ | 🟣 [#1353](https://github.com/k2-fsa/k2/pull/1353) | | [kaldi](https://github.com/kaldi-asr/kaldi) ([fork](https://github.com/AMD-Ecosystem/kaldi/tree/moat-port)) | ✅ | ✅ | 🚫 | 🟢 [#4986](https://github.com/kaldi-asr/kaldi/pull/4986) | diff --git a/projects/kaldi/status.json b/projects/kaldi/status.json index fcfdc393..764fbbed 100644 --- a/projects/kaldi/status.json +++ b/projects/kaldi/status.json @@ -7,7 +7,7 @@ "priority": 6.626, "ext_type": "cmake", "adopted_at": "2026-05-29T23:30:03Z", - "updated_at": "2026-08-07T07:05:12Z", + "updated_at": "2026-08-12T20:26:58Z", "head_sha": "014a2179c29f2c0437898a8ded09f57501eb9fb7", "depends_on": [], "pr_url": "https://github.com/kaldi-asr/kaldi/pull/4986", @@ -119,6 +119,28 @@ "first_session_at": null, "last_session_at": null } + }, + "windows-gfx1151": { + "blocked": true, + "blocked_reason": "kaldi's ROCm build is POSIX-only, and arch-independently so: src/configure configure_rocm() exits 1 unless uname is Linux and uname -m is x86_64 (src/configure:312-318), before any GPU arch is consulted. No Windows arch can build it; the same finding is already recorded for windows-gfx1101 and windows-gfx1201. Verified against AMD-Ecosystem/kaldi moat-port.", + "validated_sha": null, + "failed_sha": null, + "started_at": null, + "completed_at": null, + "updated_at": "2026-08-12T20:26:58Z", + "stats": { + "tokens_total": 0, + "tokens_approx": true, + "wall_seconds": { + "thinking": 0, + "compile": 0, + "test": 0, + "misc": 0 + }, + "session_count": 0, + "first_session_at": null, + "last_session_at": null + } } } } diff --git a/utils/moatlib.py b/utils/moatlib.py index 78be2ac7..b8c5f26a 100755 --- a/utils/moatlib.py +++ b/utils/moatlib.py @@ -717,6 +717,16 @@ def set_not_portable(name, reason, by, clear=False): def set_blocked(name, platform, blocked, reason=None): obj = load_status(name) + if platform not in obj["platforms"]: + # An absent record means this arch has recorded nothing, which is exactly the + # arch most likely to discover it cannot run the project at all -- so blocking + # one has to be able to create the row, the way a stage transition does. Only + # blocking creates it: writing a row that says "not blocked" would record an + # intention to validate somewhere, which is fleet state and not a fact about + # the port. + if not blocked: + raise ValueError(f"{name}: {platform} has recorded nothing; nothing to clear") + obj["platforms"][platform] = _platform_block(None) blk = obj["platforms"][platform] blk["blocked"] = bool(blocked) blk["blocked_reason"] = reason if blocked else None From 7563c838004188367f617cabbf4b398ec50f24aa Mon Sep 17 00:00:00 2001 From: Jeff Daily Date: Wed, 12 Aug 2026 20:50:03 +0000 Subject: [PATCH 5/6] opencv: reconcile the record with the port that exists --- projects/opencv/notes.md | 65 ++++++++++++++++++++--- projects/opencv/status.json | 103 ++++++++++++++++++++++++++++++++++-- 2 files changed, 157 insertions(+), 11 deletions(-) diff --git a/projects/opencv/notes.md b/projects/opencv/notes.md index ef713024..55f3db5e 100644 --- a/projects/opencv/notes.md +++ b/projects/opencv/notes.md @@ -2,17 +2,68 @@ A dependency fork: the OpenCV core the cv::cuda modules in contrib cannot build without. -## Why the platform row is empty +## Why the platform rows carry no validation This has no test suite of its own. The code is exercised only through the project that consumes it, so a GPU run against this repository alone would prove nothing. -The empty row is accurate, not a gap in the record. -The validation lives with **opencv_contrib**, `completed` on linux-gfx1100, linux-gfx90a, windows-gfx1101, windows-gfx1201. +The validation lives with **opencv_contrib**, `completed` on linux-gfx1100, +linux-gfx90a, windows-gfx1101, windows-gfx1201 at contrib `041d5528`. Every one of +those runs built this fork: opencv_contrib's recorded build configures cmake against +`src-core/` (this repo at `moat-port`) with `OPENCV_EXTRA_MODULES_PATH=../src/modules`, +so core and contrib are one build tree, not two. + +All four platforms here are therefore `blocked` with that reason rather than left to +derive `port-ready`. That is deliberate: from `review-passed` on, `arch_task` derives +`port-ready` for any arch with no evidence at head, which would send a validator to a +repository that cannot be validated on its own. Refresh the evidence by revalidating +**opencv_contrib**, not this project. + +## Record reconciliation 2026-08-12 + +Until today this project read `stage: unclaimed`, `head_sha: null`, no platforms and +no PR, while the port was in fact written, reviewed and in front of upstream +maintainers. Two things went wrong because of it: `dep_status("opencv")` returned +`waiting`, so a dependent (DynOSAM) would have sat in `dep-blocked` waiting on work +that was already done, and `orient.sh` kept advertising `opencv (unclaimed -> intake)`, +inviting an intake screen -- or a `scaffold` -- over a finished port. + +Corrected on a person's decision (jeff, 2026-08-12) to match the facts already +recorded in `projects/opencv_contrib/notes.md`: + +- `head_sha` -> `50f05b150687734a0d0d7084f6215de6cd3a5b95`, the tip of + `AMD-Ecosystem/opencv @ moat-port` and the head of upstream PR #29285. +- `pr_url` / `pr_number` -> https://github.com/opencv/opencv/pull/29285 (OPEN, + "[ROCm] Add AMD GPU support for cv::cuda via HIP (core)"), so `moat-checkup` tracks + it like any other open PR instead of it being invisible to the control plane. +- stage walked `unclaimed -> screened -> planning -> planned -> porting -> ported -> + review-passed`. The intermediate transitions are bookkeeping to reach the stage the + work actually reached; the work itself is real and is recorded under opencv_contrib. + +### What the evidence does and does not cover + +The review is genuine and covered this fork explicitly: opencv_contrib's review entry +records "Reviewed both fork branches base..HEAD via /pr-review: core (AMD-Ecosystem/opencv +f90ef85) and contrib", verdict review-passed. + +The validation is by consumption, and it is **not** at the current `head_sha`. The four +platform completions were taken at core `adcd50ca`/`0404733`-era commits; core then moved +`0404733 -> 50f05b1` for the PR #29285 follow-ups, under an explicit recorded decision +("jeff override: no revalidations for these", opencv_contrib notes, 2026-06-19). Those +commits are behaviour-preserving on the success path. This section states that plainly +rather than letting `review-passed` imply the head was proved on a GPU. ## Port state -The `moat-port` branch predates this project being tracked here, so the port exists -but its provenance was not recorded: no plan, no dated validation entry, no note of -which commit was tested. Treat it as real work of unverified state rather than as a -validated port. +The `moat-port` branch predates this project being tracked here, so the port's +provenance lives in `projects/opencv_contrib/notes.md` rather than here: the plan, +the dated validation entries and the per-commit test results are all recorded there, +including the two core-side fault classes found during the port (`cudev` +`simd_functions.hpp` PTX with non-saturating emulation, and `CUDART_VERSION` undefined +on HIP gating out modern paths). + +## Install as a dependency + +Consumed through opencv_contrib's two-repo build, not on its own -- see the +`## Install as a dependency` section of `projects/opencv_contrib/notes.md`. A dependent +that needs `cv::cuda` depends on **opencv_contrib**; this fork comes with it. diff --git a/projects/opencv/status.json b/projects/opencv/status.json index b9aba78d..105839b1 100644 --- a/projects/opencv/status.json +++ b/projects/opencv/status.json @@ -7,13 +7,108 @@ "priority": 0.0, "ext_type": "unknown", "adopted_at": "2026-08-06T06:58:03Z", - "updated_at": "2026-08-07T07:05:23Z", - "head_sha": null, + "updated_at": "2026-08-12T20:49:32Z", + "head_sha": "50f05b150687734a0d0d7084f6215de6cd3a5b95", "depends_on": [], "porting": null, "waivers": {}, "license_spdx": "Apache-2.0", "upstream_repo_id": 5108051, - "stage": "unclaimed", - "platforms": {} + "stage": "review-passed", + "platforms": { + "linux-gfx90a": { + "blocked": true, + "blocked_reason": "no standalone test suite: this fork is exercised only through opencv_contrib, whose build configures cmake against this repo with OPENCV_EXTRA_MODULES_PATH. Validation evidence lives with opencv_contrib (4 platforms completed at contrib 041d5528). Do not dispatch a standalone validator here; revalidate opencv_contrib instead.", + "validated_sha": null, + "failed_sha": null, + "started_at": "2026-08-12T20:49:24Z", + "completed_at": null, + "updated_at": "2026-08-12T20:49:32Z", + "stats": { + "tokens_total": 0, + "tokens_approx": true, + "wall_seconds": { + "thinking": 0, + "compile": 0, + "test": 0, + "misc": 0 + }, + "session_count": 0, + "first_session_at": null, + "last_session_at": null + }, + "last_agent": "orchestrator" + }, + "linux-gfx1100": { + "blocked": true, + "blocked_reason": "no standalone test suite: this fork is exercised only through opencv_contrib, whose build configures cmake against this repo with OPENCV_EXTRA_MODULES_PATH. Validation evidence lives with opencv_contrib (4 platforms completed at contrib 041d5528). Do not dispatch a standalone validator here; revalidate opencv_contrib instead.", + "validated_sha": null, + "failed_sha": null, + "started_at": null, + "completed_at": null, + "updated_at": "2026-08-12T20:49:32Z", + "stats": { + "tokens_total": 0, + "tokens_approx": true, + "wall_seconds": { + "thinking": 0, + "compile": 0, + "test": 0, + "misc": 0 + }, + "session_count": 0, + "first_session_at": null, + "last_session_at": null + } + }, + "windows-gfx1101": { + "blocked": true, + "blocked_reason": "no standalone test suite: this fork is exercised only through opencv_contrib, whose build configures cmake against this repo with OPENCV_EXTRA_MODULES_PATH. Validation evidence lives with opencv_contrib (4 platforms completed at contrib 041d5528). Do not dispatch a standalone validator here; revalidate opencv_contrib instead.", + "validated_sha": null, + "failed_sha": null, + "started_at": null, + "completed_at": null, + "updated_at": "2026-08-12T20:49:32Z", + "stats": { + "tokens_total": 0, + "tokens_approx": true, + "wall_seconds": { + "thinking": 0, + "compile": 0, + "test": 0, + "misc": 0 + }, + "session_count": 0, + "first_session_at": null, + "last_session_at": null + } + }, + "windows-gfx1201": { + "blocked": true, + "blocked_reason": "no standalone test suite: this fork is exercised only through opencv_contrib, whose build configures cmake against this repo with OPENCV_EXTRA_MODULES_PATH. Validation evidence lives with opencv_contrib (4 platforms completed at contrib 041d5528). Do not dispatch a standalone validator here; revalidate opencv_contrib instead.", + "validated_sha": null, + "failed_sha": null, + "started_at": null, + "completed_at": null, + "updated_at": "2026-08-12T20:49:32Z", + "stats": { + "tokens_total": 0, + "tokens_approx": true, + "wall_seconds": { + "thinking": 0, + "compile": 0, + "test": 0, + "misc": 0 + }, + "session_count": 0, + "first_session_at": null, + "last_session_at": null + } + } + }, + "pr_url": "https://github.com/opencv/opencv/pull/29285", + "pr_number": 29285, + "pr_opened_at": "2026-08-12T20:49:25Z", + "pr_state": "open", + "published_sha": "50f05b150687734a0d0d7084f6215de6cd3a5b95" } From 838867137792328203c7d260405bd916db489017 Mon Sep 17 00:00:00 2001 From: Jeff Daily Date: Thu, 13 Aug 2026 02:46:57 +0000 Subject: [PATCH 6/6] opencv: block linux-gfx942 (no standalone test suite, validate via opencv_contrib) --- projects/opencv/notes.md | 31 +++++++++++++++++++++++++++++++ projects/opencv/stats.jsonl | 2 ++ projects/opencv/status.json | 24 +++++++++++++++++++++++- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 projects/opencv/stats.jsonl diff --git a/projects/opencv/notes.md b/projects/opencv/notes.md index 55f3db5e..bd3fb70f 100644 --- a/projects/opencv/notes.md +++ b/projects/opencv/notes.md @@ -62,6 +62,37 @@ including the two core-side fault classes found during the port (`cudev` `simd_functions.hpp` PTX with non-saturating emulation, and `CUDART_VERSION` undefined on HIP gating out modern paths). +## Validation 2026-08-13 (linux-gfx942) + +Dispatched as validator for a new platform, linux-gfx942 (gfx942, wave64, MI300X), +which did not yet appear in `platforms`. Same situation as the four existing rows: +this fork carries no standalone test suite of its own, so a real-GPU run against +`AMD-Ecosystem/opencv @ moat-port` alone would prove nothing -- there is no +`opencv_test_*` binary this repo can produce without `OPENCV_EXTRA_MODULES_PATH` +pointed at contrib, and the fork checkout was skipped for exactly that reason (no +build/compile/test commands were run; `wall_seconds` stays zero here by design). + +Marked `linux-gfx942` `blocked` with the identical reasoning already recorded for +linux-gfx90a / linux-gfx1100 / windows-gfx1101 / windows-gfx1201: revalidate +**opencv_contrib**, not this project, since its two-repo build tree is what +actually exercises this fork's HIP/cudev code. + +Checked opencv_contrib's own record while here: `linux-gfx942` is not present in +its `platforms` map either, so the wave64 gate for gfx942 specifically has no +opencv_contrib evidence yet. Not this validator's concern to fix -- wave64 is +already satisfied for opencv_contrib by linux-gfx90a (`completed`, +`validated_sha=041d5528...`) at the current head, so `pr-ready` is unaffected. +Noting it only so a future opencv_contrib dispatch on this card knows what is and +isn't covered. + +`git -C projects/opencv/src status --porcelain`: no local checkout exists (nothing +built), so the integrity gate is trivially satisfied -- no tracked source/build +edits. + +Commands run: `python3 utils/moatlib.py set-blocked opencv linux-gfx942 "..."`. +No PR interaction (PR #29285 is open; `moat-port` frozen, and this round pushed +nothing to the fork). + ## Install as a dependency Consumed through opencv_contrib's two-repo build, not on its own -- see the diff --git a/projects/opencv/stats.jsonl b/projects/opencv/stats.jsonl new file mode 100644 index 00000000..aa8cf8ab --- /dev/null +++ b/projects/opencv/stats.jsonl @@ -0,0 +1,2 @@ +{"kind":"session","ts":"2026-08-13T02:46:23Z","epoch":1786589183.786720575,"event":"start","platform":"linux-gfx942"} +{"kind":"session","ts":"2026-08-13T02:46:52Z","epoch":1786589212.607630635,"event":"end","platform":"linux-gfx942"} diff --git a/projects/opencv/status.json b/projects/opencv/status.json index 105839b1..1e40e8b3 100644 --- a/projects/opencv/status.json +++ b/projects/opencv/status.json @@ -7,7 +7,7 @@ "priority": 0.0, "ext_type": "unknown", "adopted_at": "2026-08-06T06:58:03Z", - "updated_at": "2026-08-12T20:49:32Z", + "updated_at": "2026-08-13T02:46:29Z", "head_sha": "50f05b150687734a0d0d7084f6215de6cd3a5b95", "depends_on": [], "porting": null, @@ -104,6 +104,28 @@ "first_session_at": null, "last_session_at": null } + }, + "linux-gfx942": { + "blocked": true, + "blocked_reason": "no standalone test suite: this fork is exercised only through opencv_contrib, whose build configures cmake against this repo with OPENCV_EXTRA_MODULES_PATH. Validation evidence lives with opencv_contrib (4 platforms completed at contrib 041d5528; linux-gfx942 not yet dispatched there either). Do not dispatch a standalone validator here; revalidate opencv_contrib instead.", + "validated_sha": null, + "failed_sha": null, + "started_at": null, + "completed_at": null, + "updated_at": "2026-08-13T02:46:29Z", + "stats": { + "tokens_total": 0, + "tokens_approx": true, + "wall_seconds": { + "thinking": 0, + "compile": 0, + "test": 0, + "misc": 0 + }, + "session_count": 0, + "first_session_at": null, + "last_session_at": null + } } }, "pr_url": "https://github.com/opencv/opencv/pull/29285",