From b076e61e5e1ca59c59ba0951f94e27e98dfddb2e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 20:22:01 -0400 Subject: [PATCH 01/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 145 +---- dsl/gunbc/ci_github_actions_workflow.dag | 42 +- .../check-workflow-path-regex-inventory.sh | 211 +------ src/v3/compiler/src/cementing_dispatch.rs | 74 ++- src/v3/compiler/src/gunbc_ci.rs | 551 ++++++++++++++++++ .../t_ci_workflow_as_data_demo_test.rs | 62 +- 6 files changed, 728 insertions(+), 357 deletions(-) create mode 100644 src/v3/compiler/src/gunbc_ci.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c6e9b0152e..b9856504617 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -184,79 +184,13 @@ jobs: - name: v3 bootstrap snapshot freshness gate (--verify) run: cargo run -p v3-compiler --features bootstrap-regen-fresh --bin regen_bootstrap -- --verify - # Layer 1 CI mitigation (bridge-debt; gunbc#846 2026-05-11): coarse path - # filter so docs-only PRs skip the heavy `v3` job (~67m wall). Computes a - # single boolean output `code` = "PR touches anything outside the docs - # allowlist." Docs allowlist: `docs/**` + root `*.md` files. Anything else - # (src/, dsl/, scripts/, .github/, Cargo.lock, rust-toolchain.toml, etc.) - # counts as a code change and runs the full v3 job. - # - # **Dissolution trigger**: gate `ci_uses_provable_minimal_affected_set_selection` - # (**R4.B full delivery** per `docs/design-affected-set-lens.md` §5 "CI integration - # sketch (deferred to R4 full delivery)"; the affected-set lens prototype is - # pre-R3-close at gunbc#2699 but CI integration is R4-scoped). Once the lens - # computes provable minimal test sets AND the CI integration lands, this coarse - # filter is replaced by the structural lens output. Delete this job + `needs: - # [changes]` on `v3` at that point. - # - # Note on branch protection: when `v3` skips (docs-only), `self_host_ratchet` - # (`needs: [v3]`) would also skip. To keep its required-check name green for - # branch protection, its `if:` was widened below to accept `v3.result == - # 'skipped'`. - changes: - if: github.event.pull_request.draft != true - runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }} - timeout-minutes: 3 - outputs: - code: ${{ steps.filter.outputs.code }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Detect non-docs changes - id: filter - run: | - set -euo pipefail - if [ "${{ github.event_name }}" = "push" ]; then - echo "push event — always run heavy v3 job" - echo "code=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - git fetch origin main:refs/remotes/origin/main - changed=$(git diff --name-only origin/main...HEAD) - echo "Changed files (first 200):" - echo "$changed" | head -200 - # Docs-only allowlist: anything under docs/ OR a root-level *.md - # file (no slash before .md). Examples that count as docs: - # docs/audit/foo.md ✓ - # docs/design-x.md ✓ - # WISHLIST.md ✓ (root *.md) - # ROADMAP.md ✓ - # Examples that count as code (run full v3): - # src/v3/compiler/src/foo.rs - # dsl/std/lens_foo.dag - # scripts/check-foo.sh - # .github/workflows/ci.yml (workflow self-edit) - # src/v3/compiler/tests/README.md (nested .md, not root) - non_docs=$(echo "$changed" | grep -vE '^(docs/.*|[^/]+\.md)$' || true) - if [ -n "$non_docs" ]; then - echo "Non-docs files detected; v3 will run full heavy compute:" - echo "$non_docs" | head -50 - echo "code=true" >> "$GITHUB_OUTPUT" - else - echo "Docs-only PR (allowlist: docs/** + root *.md) — v3 will skip" - echo "(Layer 1 CI mitigation; dissolution trigger = affected-set lens CI integration)" - echo "code=false" >> "$GITHUB_OUTPUT" - fi - + # Merge-blocking heavy compute always runs on non-draft PRs and on pushes to + # main. Docs-only skip was removed under gate `ci_uses_affected_set_selection` + # (program row 103): YAML path-regex selection is not authoritative; future + # narrowing belongs in the BinaryShim runner (`select_affected_gates_for_binary_shim` + # + PR #2713 lens receipts per `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md`). v3: - # Job-level `if:` gates on the `changes` filter so docs-only PRs skip the - # heavy compute entirely. Pushes to main always run the full job (no PR - # base to diff against). - if: github.event.pull_request.draft != true && (needs.changes.outputs.code == 'true' || github.event_name == 'push') - needs: [changes] + if: github.event.pull_request.draft != true # Heavy compute (cargo release builds, full v3 test suite at the # 4000s transitional budget + clippy + lens gates). 2-core ubuntu-latest blows # the 60min cap reliably; `ubicloud-standard-8` (8 vCPU, ~$0.0256/hr) finishes @@ -602,34 +536,10 @@ jobs: # R1 `pb_self_compile_fixed_point` pass/fail still comes from `verification.dag` + # `test_runner` — this job's continue-on-error posture does not redefine that gate. self_host_ratchet: - # Always schedule this job name on PRs so branch-protection "required checks" get a - # definitive **success** after `v3` (skipped jobs / workflow-wide cancels were surfacing - # as merge-blocking reds on #2681). The heavy DB-8 matrix still runs **only** on - # `push` to `main`; PRs execute a cheap stub via the `DB-8 stub` step below. - # - # Layer 1 CI mitigation widens this `if:`: when `v3` is skipped on a docs-only - # PR (via the `changes` filter job), this stub must still report green so its - # required-check name doesn't block merges. The `always() && ... != 'failure'` - # pattern lets this job run when `v3` either succeeded OR was skipped, but not - # when `v3` actually failed. - # - # **Fail-closed under filter failure** (openai-pro REQUEST_CHANGES on PR #2718, - # then codex BLOCKING on PR #2718 line 579 caught the actual fail-open): - # - # Original openai-pro fix used `if: ... && needs.changes.result == 'success'`, - # intending changes-failure → self_host_ratchet skipped → branch-protection - # incomplete signal. **But GitHub branch protection treats SKIPPED required - # checks as SUCCESS by default** (per docs/en/pull-requests/.../troubleshooting- - # required-status-checks#handling-skipped-but-required-checks). So skipping on - # filter-failure was still phantom-green. - # - # **True fail-closed**: the job MUST RUN on every PR (`if: ${{ always() && ... }}` - # with no result-checks gating the JOB), and an EXPLICIT step fails when the - # gate evidence is incomplete. A failed step → job FAILURE → required-check RED - # → branch-protection BLOCKS. Job-level `continue-on-error: true` (kept below) - # only governs workflow conclusion, not job conclusion reported to branch - # protection; individual DB-8 staging steps have their own step-level - # continue-on-error to preserve their non-blocking semantics. + # Always schedule on non-draft PRs (and on pushes, where `pull_request` is absent) + # so branch-protection required checks get a definitive result after `v3`. + # The heavy DB-8 matrix still runs only on `push` to `main`; PRs hit the stub below. + # First step validates `v3` outcome (fail-closed on failure/cancel). if: ${{ always() && github.event.pull_request.draft != true }} # Same sizing rationale as `v3` above: DB-8 release builds + determinism_test (5× matrix) + # `self_host_fixed_point` run on `ubicloud-standard-8` (#1814); small/default runners saw @@ -638,41 +548,20 @@ jobs: # `rust-toolchain.toml` + no workflow `toolchain:` input.) runs-on: ubicloud-standard-8 timeout-minutes: 60 - # `changes` added to `needs:` so the fail-closed `if:` clause above can - # reference `needs.changes.result` (openai-pro REQUEST_CHANGES fix). Without - # the `needs:` declaration the `result` accessor is unavailable and the - # gate degenerates to fail-open under filter failure. - needs: [v3, changes] + needs: [v3] continue-on-error: true steps: - # **Fail-closed gate-evidence validation** (codex BLOCKING on PR #2718 line 579 fix). - # Reports explicit step FAILURE → job FAILURE → required-check RED when the - # Layer 1 filter (`changes`) failed or was cancelled, or when `v3` failed/cancelled. - # Job conclusion FAILURE propagates to branch protection (skipped does NOT — that's - # the bug this fixes). No `continue-on-error` on this step; its failure must surface. - # - # Allowed `v3.result` values that the validation accepts: - # - `success`: v3 ran cleanly (CI green) - # - `skipped`: v3 was skipped because `changes` proved docs-only (`code=false`) - # Allowed `changes.result` values: - # - `success`: filter completed (regardless of `code` value) - # Anything else → fail with explicit diagnostic (P3 fail-closed / C-8). - - name: Validate gate evidence (fail-closed under filter/v3 failure) + - name: Validate gate evidence (fail-closed under v3 failure) run: | - echo "Gate evidence: changes.result=${{ needs.changes.result }} v3.result=${{ needs.v3.result }}" - if [ "${{ needs.changes.result }}" != "success" ]; then - echo "::error::Layer 1 filter (changes job) did not succeed (result=${{ needs.changes.result }})." - echo "::error::Cannot determine v3 skip legitimacy — skipped required-checks default to SUCCESS" - echo "::error::in GitHub branch protection (https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks)," - echo "::error::so an explicit step failure here surfaces the missing-evidence as a diagnostic" - echo "::error::per P3 fail-closed / C-8 discipline." - exit 1 - fi + echo "Gate evidence: v3.result=${{ needs.v3.result }}" if [ "${{ needs.v3.result }}" = "failure" ] || [ "${{ needs.v3.result }}" = "cancelled" ]; then echo "::error::v3 job did not succeed (result=${{ needs.v3.result }}); ratchet cannot claim green." exit 1 fi - # Either v3.result == success (ran cleanly) or v3.result == skipped (legit docs-only via changes.code=false) + if [ "${{ needs.v3.result }}" != "success" ]; then + echo "::error::v3 job has unexpected result (result=${{ needs.v3.result }}); expected success after Layer-2 path-regex removal." + exit 1 + fi echo "Gate evidence validated; ratchet may proceed." - name: Checkout diff --git a/dsl/gunbc/ci_github_actions_workflow.dag b/dsl/gunbc/ci_github_actions_workflow.dag index 335ab96f32d..26e54b03d1a 100644 --- a/dsl/gunbc/ci_github_actions_workflow.dag +++ b/dsl/gunbc/ci_github_actions_workflow.dag @@ -236,38 +236,6 @@ data gunbc_ci_github_actions_workflow: Workflow = { timeout_minutes: Some { value: 25 }, continue_on_error: false, concurrency: none - }, { - id: "changes", - name: none, - runner: RunsOnExpression { expression: "${{ vars.CI_RUNNER || 'ubuntu-latest' }}" }, - steps: [UsesStep { - name: Some { value: "Checkout" }, - id: none, - uses: { owner: "actions", repo: "checkout", ref: "v4" }, - with: Some { value: { "fetch-depth": "0" } }, - env: none, - if_condition: none, - continue_on_error: false, - timeout_minutes: none - }, RunStep { - name: Some { value: "Detect non-docs changes" }, - id: Some { value: "filter" }, - run: "set -euo pipefail\nif [ \"${{ github.event_name }}\" = \"push\" ]; then\n echo \"push event — always run heavy v3 job\"\n echo \"code=true\" >> \"$GITHUB_OUTPUT\"\n exit 0\nfi\ngit fetch origin main:refs/remotes/origin/main\nchanged=$(git diff --name-only origin/main...HEAD)\necho \"Changed files (first 200):\"\necho \"$changed\" | head -200\n# Docs-only allowlist: anything under docs/ OR a root-level *.md\n# file (no slash before .md). Examples that count as docs:\n# docs/audit/foo.md ✓\n# docs/design-x.md ✓\n# WISHLIST.md ✓ (root *.md)\n# ROADMAP.md ✓\n# Examples that count as code (run full v3):\n# src/v3/compiler/src/foo.rs\n# dsl/std/lens_foo.dag\n# scripts/check-foo.sh\n# .github/workflows/ci.yml (workflow self-edit)\n# src/v3/compiler/tests/README.md (nested .md, not root)\nnon_docs=$(echo \"$changed\" | grep -vE '^(docs/.*|[^/]+\\.md)$' || true)\nif [ -n \"$non_docs\" ]; then\n echo \"Non-docs files detected; v3 will run full heavy compute:\"\n echo \"$non_docs\" | head -50\n echo \"code=true\" >> \"$GITHUB_OUTPUT\"\nelse\n echo \"Docs-only PR (allowlist: docs/** + root *.md) — v3 will skip\"\n echo \"(Layer 1 CI mitigation; dissolution trigger = affected-set lens CI integration)\"\n echo \"code=false\" >> \"$GITHUB_OUTPUT\"\nfi\n", - shell: Bash, - env: none, - working_directory: none, - if_condition: none, - continue_on_error: false, - timeout_minutes: none - }], - needs: [], - env: none, - outputs: Some { value: { "code": "${{ steps.filter.outputs.code }}" } }, - if_condition: Some { value: "github.event.pull_request.draft != true" }, - strategy: none, - timeout_minutes: Some { value: 3 }, - continue_on_error: false, - concurrency: none }, { id: "v3", name: none, @@ -507,10 +475,10 @@ data gunbc_ci_github_actions_workflow: Workflow = { continue_on_error: false, timeout_minutes: none }], - needs: ["changes"], + needs: [], env: none, outputs: none, - if_condition: Some { value: "github.event.pull_request.draft != true && (needs.changes.outputs.code == 'true' || github.event_name == 'push')" }, + if_condition: Some { value: "github.event.pull_request.draft != true" }, strategy: none, timeout_minutes: Some { value: 120 }, continue_on_error: false, @@ -520,9 +488,9 @@ data gunbc_ci_github_actions_workflow: Workflow = { name: none, runner: SelfHosted { labels: ["ubicloud-standard-8"] }, steps: [RunStep { - name: Some { value: "Validate gate evidence (fail-closed under filter/v3 failure)" }, + name: Some { value: "Validate gate evidence (fail-closed under v3 failure)" }, id: none, - run: "echo \"Gate evidence: changes.result=${{ needs.changes.result }} v3.result=${{ needs.v3.result }}\"\nif [ \"${{ needs.changes.result }}\" != \"success\" ]; then\n echo \"::error::Layer 1 filter (changes job) did not succeed (result=${{ needs.changes.result }}).\"\n echo \"::error::Cannot determine v3 skip legitimacy — skipped required-checks default to SUCCESS\"\n echo \"::error::in GitHub branch protection (https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks),\"\n echo \"::error::so an explicit step failure here surfaces the missing-evidence as a diagnostic\"\n echo \"::error::per P3 fail-closed / C-8 discipline.\"\n exit 1\nfi\nif [ \"${{ needs.v3.result }}\" = \"failure\" ] || [ \"${{ needs.v3.result }}\" = \"cancelled\" ]; then\n echo \"::error::v3 job did not succeed (result=${{ needs.v3.result }}); ratchet cannot claim green.\"\n exit 1\nfi\n# Either v3.result == success (ran cleanly) or v3.result == skipped (legit docs-only via changes.code=false)\necho \"Gate evidence validated; ratchet may proceed.\"\n", + run: "echo \"Gate evidence: v3.result=${{ needs.v3.result }}\"\nif [ \"${{ needs.v3.result }}\" = \"failure\" ] || [ \"${{ needs.v3.result }}\" = \"cancelled\" ]; then\n echo \"::error::v3 job did not succeed (result=${{ needs.v3.result }}); ratchet cannot claim green.\"\n exit 1\nfi\nif [ \"${{ needs.v3.result }}\" != \"success\" ]; then\n echo \"::error::v3 job has unexpected result (result=${{ needs.v3.result }}); expected success after Layer-2 path-regex removal.\"\n exit 1\nfi\necho \"Gate evidence validated; ratchet may proceed.\"\n", shell: Bash, env: none, working_directory: none, @@ -597,7 +565,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { continue_on_error: false, timeout_minutes: none }], - needs: ["v3", "changes"], + needs: ["v3"], env: none, outputs: none, if_condition: Some { value: "${{ always() && github.event.pull_request.draft != true }}" }, diff --git a/scripts/check-workflow-path-regex-inventory.sh b/scripts/check-workflow-path-regex-inventory.sh index 396f28f5ace..e3d58f78c9f 100755 --- a/scripts/check-workflow-path-regex-inventory.sh +++ b/scripts/check-workflow-path-regex-inventory.sh @@ -1,30 +1,13 @@ #!/usr/bin/env bash -# T-WAD Slice 7 — observational ratchet pinning the **current** authoritative -# path-regex selection in .github/workflows/*.yml. +# T-WAD Slice 7 — post-dissolution ratchet: authoritative path-regex selection must +# NOT reappear in `.github/workflows/*.{yml,yaml}` after gate +# `ci_uses_affected_set_selection` (program row 103) removed the Layer-2 bridge. # -# This script is FAIL-CLOSED in BOTH directions: -# -# (a) If a NEW authoritative path-regex selection appears outside the -# inventory documented in -# `docs/design-t-wad-slice-7-implementation-prequeue.md` §3, the script -# fails so the operator must either widen the inventory + canvas or -# remove the new bridge. -# -# (b) If an inventoried selection site disappears BEFORE the T-WAD Slice 7 -# implementation PR lands the BinaryShim runner consuming PR #2713 -# affected-set lens output, the script also fails — silent removal -# would leave authoritative selection in an inconsistent state. -# -# This script is NOT wired into .github/workflows/ci.yml by the PR that -# introduces it (that would itself be a CI behavior change). It is intended -# to be invoked manually by reviewers and by the eventual Slice 7 -# implementation PR as part of its dissolution receipt. -# -# Status today: gate `ci_uses_affected_set_selection` (row 103) is NOT closer -# to PASSING because of this script — see the prequeue doc §2. +# Fail-closed: forbidden fingerprints → non-zero exit. +# Pre-dissolution behavior (require inventoried bridges) lived in git history; +# see `docs/design-t-wad-slice-7-implementation-prequeue.md`. # # Usage: scripts/check-workflow-path-regex-inventory.sh -# Exit 0 = inventory matches expectation; non-zero = drift detected. set -euo pipefail @@ -32,165 +15,28 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$ROOT" -INVENTORY_DOC="docs/design-t-wad-slice-7-implementation-prequeue.md" -CI_YML=".github/workflows/ci.yml" - -# Single source of truth for "GitHub Actions workflow file" universe. Both -# `.yml` and `.yaml` are honored by GitHub Actions (per -# https://docs.github.com/actions). Every detector below uses this helper -# instead of inlining a glob, so a new file extension cannot create a -# fail-open extension gap. -workflow_files_nul() { - git ls-files -z '.github/workflows/*.yml' '.github/workflows/*.yaml' -} - -# Stray .yaml warning: today the repo has zero workflow .yaml files. If one -# appears in future, that is fine on its own — but the bigger picture, per -# §3 inventory + Slice 7 dissolution path, is that *all* workflow files -# (regardless of extension) feed both detectors below. - violations=0 note() { echo "check-workflow-path-regex-inventory: $*" >&2; } fail() { note "FAIL: $*"; violations=$((violations + 1)); } -if [[ ! -f "$CI_YML" ]]; then - fail "$CI_YML not found — inventory cannot be verified" -fi - -# ---- Inventoried site #1: the `changes:` job docs-only allowlist grep ------- -# Fingerprint: the literal regex string used by the `changes:` job to detect -# non-docs files. Pinned by exact substring to avoid false positives from -# unrelated edits elsewhere in the workflow. -PATH_REGEX_FILTER="grep -vE '^(docs/.*|[^/]+\\.md)\$'" -if ! grep -qF "$PATH_REGEX_FILTER" "$CI_YML"; then - fail "expected path-regex docs-only filter missing from $CI_YML: - searched literal: $PATH_REGEX_FILTER - see $INVENTORY_DOC §3 row #1. - If you are landing the T-WAD Slice 7 implementation PR (BinaryShim - runner consuming PR #2713 lens output), update / retire this script - as part of the dissolution receipt — do not just delete the filter." -fi +workflow_files_nul() { + git ls-files -z '.github/workflows/*.yml' '.github/workflows/*.yaml' +} -# ---- Inventoried site #2: the `v3:` job `if:` consuming `outputs.code` ------ -# Fingerprint: the `needs.changes.outputs.code == 'true'` substring inside -# an `if:` line. This is the gate that turns site #1 into authoritative -# selection (skip-when-false on PR events). -if ! grep -E "^\s*if:.*needs\.changes\.outputs\.code" "$CI_YML" >/dev/null; then - fail "expected v3-job if-gate on needs.changes.outputs.code missing from $CI_YML: - see $INVENTORY_DOC §3 row #2. - Same dissolution rule as row #1." -fi +DOCS_ONLY_FILTER="grep -vE '^(docs/.*|[^/]+\.md)$'" -# ---- Drift detector: NEW authoritative path-regex selection ---------------- -# Heuristic: count `git diff --name-only` invocations across ALL workflow -# files. The inventory (§3) anchors a single expected occurrence — the -# inventoried `changes:` job in ci.yml with the literal anchor -# `origin/main...HEAD`. Any occurrence beyond that — whether in ci.yml itself -# (e.g., the `changes:` job grows a second selector, or a new job adds its -# own diff) OR in a sibling workflow file — is a candidate new path-regex -# bridge that the inventory does not yet describe. -# -# This intentionally scans `ci.yml` as well as siblings so the inventoried -# file is not free to grow un-inventoried selection (closes the fail-open -# gap that the v1 of this script had). -INVENTORIED_DIFF_ANCHOR="git diff --name-only origin/main...HEAD" -expected_diff_count=1 # exactly the §3 row #1 invocation -actual_diff_count=0 -extra_locations=() -while IFS= read -r line; do - [[ -z "$line" ]] && continue - actual_diff_count=$((actual_diff_count + 1)) - # Extract "file:lineno:content" from `grep -n` output across files. - if [[ "$line" != *"$INVENTORIED_DIFF_ANCHOR"* ]]; then - extra_locations+=("$line") +while IFS= read -r -d '' f; do + if m=$(grep -nF "git diff --name-only" "$f" 2>/dev/null || true); [[ -n "$m" ]]; then + fail "forbidden 'git diff --name-only' in $f — BinaryShim + lens receipts are authoritative:\n$m" fi -done < <(workflow_files_nul | xargs -0 grep -nH "git diff --name-only" 2>/dev/null || true) - -# Independent anchor-host check: the §3 row #1 invariant says the inventoried -# diff anchor LIVES IN ci.yml. The earlier PATH_REGEX_FILTER grep at site #1 -# pins a DIFFERENT literal (the docs-only allowlist), so a refactor that -# removed the `git diff --name-only origin/main...HEAD` line but kept the -# allowlist regex would slip past the earlier check. Enforce the anchor-host -# invariant explicitly here so cursor 10271's failure modes are both closed. -inventoried_present=0 -if grep -qF "$INVENTORIED_DIFF_ANCHOR" "$CI_YML" 2>/dev/null; then - inventoried_present=1 -fi -if (( inventoried_present == 0 )); then - fail "§3 row #1 anchor missing from $CI_YML: - expected literal: $INVENTORIED_DIFF_ANCHOR - per $INVENTORY_DOC §3 + §4 fail-closed contract, this anchor must - live in ci.yml until the BinaryShim runner replacement (post–Slice 5) - is wired. If you're landing the Slice 7 implementation PR, retire - this script as part of the dissolution receipt — do not just remove - the anchor." -fi - -# Count invariant: actual_diff_count must equal expected_diff_count exactly. -# Both directions fail-closed (per §4 + INVARIANTS P3): overflow = candidate -# new bridge; underflow = inventoried bridge removed before replacement. -if (( actual_diff_count > expected_diff_count )); then - if (( ${#extra_locations[@]} == 0 )); then - # Every overflow match still contains the inventoried anchor verbatim — - # i.e., a DUPLICATE copy of the anchored invocation. Treat as a new - # bridge. - fail "duplicate-anchor count overflow: found $actual_diff_count occurrences of - 'git diff --name-only' but expected $expected_diff_count. Every overflow - line still matches the inventoried anchor verbatim, suggesting a copy - of the §3 row #1 invocation was added (same literal, second site). - Either route selection through the BinaryShim runner (post–Slice 5) - or, if event-orthogonal, document in $INVENTORY_DOC §3 and bump - 'expected_diff_count' in this script." - else - for loc in "${extra_locations[@]}"; do - fail "new path-regex selection candidate (un-inventoried 'git diff --name-only'): $loc - If this is genuinely orthogonal to affected-set selection, add it to - $INVENTORY_DOC §3 with rationale and bump 'expected_diff_count' in - this script. Otherwise route selection through the BinaryShim runner - (post–Slice 5)." - done + if m=$(grep -nE "needs\.changes\.outputs" "$f" 2>/dev/null || true); [[ -n "$m" ]]; then + fail "forbidden needs.changes.outputs bridge in $f:\n$m" fi -elif (( actual_diff_count < expected_diff_count )); then - fail "count underflow: found $actual_diff_count occurrence(s) of - 'git diff --name-only' across all workflow files but expected - $expected_diff_count (the §3 row #1 inventoried anchor). The - inventoried bridge appears to have been removed before BinaryShim - replacement is wired. Per $INVENTORY_DOC §4 fail-closed contract, - do not just delete the anchor — retire this script as part of the - Slice 7 dissolution receipt." -fi + if m=$(grep -nF "$DOCS_ONLY_FILTER" "$f" 2>/dev/null || true); [[ -n "$m" ]]; then + fail "forbidden docs-only path-regex filter in $f:\n$m" + fi +done < <(workflow_files_nul) -# ---- Drift detector: OTHER path-regex selection mechanisms ----------------- -# A second class of authoritative path-regex selection that does NOT route -# through `git diff --name-only`: -# -# * Trigger-level `paths:` / `paths-ignore:` filters on `on: push:` / -# `on: pull_request:` (skip the workflow when no matching files change). -# * `dorny/paths-filter` action usage (popular changed-files filter action). -# * `tj-actions/changed-files` action usage (alternative changed-files -# action). -# * The literal substring `paths-filter` used in step uses-clauses. -# -# Baseline today (`.github/workflows/*.yml`): zero occurrences of any of -# these. The Slice 7 dissolution surface is meant to consume the PR #2713 -# affected-set lens via the BinaryShim runner — not to grow a second -# parallel selection authority. Any introduction → fail. -# -# If a legitimate event-orthogonal use of `paths:` is needed in future, -# document it in $INVENTORY_DOC §3 and add the specific file:line to an -# explicit allowlist here (mirroring the `expected_diff_count` pattern -# above). The default stance is fail-closed. -# Trigger-level `paths:` / `paths-ignore:` detector (awk state-machine). -# Only flags `paths:` / `paths-ignore:` keys that appear UNDER -# `on:` → `push:` / `pull_request:` / `pull_request_target:` (the only YAML -# locations where these keys carry authoritative path-regex selection -# semantics). Step-input `paths:` keys (e.g., `with:` blocks under -# `dorny/paths-filter`) are intentionally NOT flagged here — they are step -# inputs, not workflow-trigger filters; the dorny action itself is caught by -# the separate action-name detector below. -# -# This precise scoping resolves the doc-vs-impl mismatch raised by cursor -# review 10280 (P2 / Practice 5 single-authority alignment). trigger_paths_awk=' function indent_of(s, l) { l = match(s, /[^ ]/); return l > 0 ? l - 1 : -1 } { @@ -225,29 +71,16 @@ function indent_of(s, l) { l = match(s, /[^ ]/); return l > 0 ? l - 1 : -1 } while IFS= read -r match; do [[ -z "$match" ]] && continue - fail "new trigger-level paths:/paths-ignore: selection candidate: $match - Authoritative path-regex selection under on:push / on:pull_request / - on:pull_request_target. Either dissolve via the BinaryShim runner - (post–Slice 5) or, if event-orthogonal, document in $INVENTORY_DOC §3 - and add an explicit allowlist entry to this script." + fail "trigger-level paths:/paths-ignore: selection candidate: $match" done < <( while IFS= read -r -d '' f; do awk "$trigger_paths_awk" "$f" done < <(workflow_files_nul) ) -# Action-name detector for non-trigger-level non-diff path-regex selection: -# popular changed-files action wrappers that read changed paths and emit -# outputs which downstream jobs gate on. Uses-clause substrings are a tight -# fingerprint with no plausible false positives. while IFS= read -r match; do [[ -z "$match" ]] && continue - fail "new changed-files action use detected: $match - Matches one of: dorny/paths-filter, tj-actions/changed-files, or - a 'paths-filter@' uses-clause substring. Either dissolve via the - BinaryShim runner (post–Slice 5) or, if event-orthogonal, document - in $INVENTORY_DOC §3 and add an explicit allowlist entry to this - script." + fail "changed-files action wrapper candidate: $match" done < <( workflow_files_nul \ | xargs -0 grep -nHE "dorny/paths-filter|tj-actions/changed-files|paths-filter@" \ @@ -259,4 +92,4 @@ if (( violations > 0 )); then exit 1 fi -echo "check-workflow-path-regex-inventory: ok (2/2 inventoried sites present, no new bridges)" +echo "check-workflow-path-regex-inventory: ok (no authoritative path-regex selection in workflows)" diff --git a/src/v3/compiler/src/cementing_dispatch.rs b/src/v3/compiler/src/cementing_dispatch.rs index e432b4bafa7..825fc55e8c1 100644 --- a/src/v3/compiler/src/cementing_dispatch.rs +++ b/src/v3/compiler/src/cementing_dispatch.rs @@ -528,7 +528,9 @@ pub mod gunbc_ci { //! [`CiWorkflowDagInput`] + [`CiWorkflowDiff`] only. Git diffs, path-regex, env, //! and PR #2713 lens receipts are upstream responsibilities; they must be //! mapped into [`CiWorkflowDiff`] (or a superset-equivalent touch set) before - //! calling [`select_affected_gates`]. + //! calling [`select_affected_gates`]. **BinaryShim / gate #103:** the adapter + //! boundary [`CiBinaryShimAffectedSetReceipt`] + [`select_affected_gates_for_binary_shim`] + //! consumes PR #2713 lens output after upstream lowers affected nodes into gate ids. //! //! **Verifier ratchet witness (Phase C scaffolding)** — `docs/design-ci-workflow-substrate-shape-2026-05-12.md` //! S5 + prequeue §5.2: monotone **inclusion** under enlarging the touched-id @@ -721,6 +723,35 @@ pub mod gunbc_ci { } } + /// Receipt boundary for **BinaryShim** gate dispatch after the PR #2713 affected-set + /// stack (and any obligation metadata) has been lowered into **`CIGate.id` strings** + /// from `dsl/gunbc/ci.dag` — not raw `NodeRef` keys (those are mapped upstream per + /// `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §1.4). + /// + /// When `narrowing_available` is `false`, selection fails closed to the full gate + /// roster ([`CiWorkflowDiff::TouchAll`]; canvas §3: unknown dimension / missing + /// receipt / unbuildable DAG pair). + #[derive(Debug, Clone, PartialEq, Eq, Default)] + pub struct CiBinaryShimAffectedSetReceipt { + /// `false` forces [`CiWorkflowDiff::TouchAll`] regardless of `proven_direct_gate_touches`. + pub narrowing_available: bool, + /// Gate ids the lens stack proved directly touched (seed before symmetric expansion). + pub proven_direct_gate_touches: BTreeSet, + } + + /// `BinaryShim` entry: map a post-lens receipt into [`select_affected_gates`]. + pub fn select_affected_gates_for_binary_shim( + dag: &CiWorkflowDagInput, + receipt: &CiBinaryShimAffectedSetReceipt, + ) -> Result, CiAffectedGatesError> { + let diff = if receipt.narrowing_available { + CiWorkflowDiff::TouchedGates(receipt.proven_direct_gate_touches.clone()) + } else { + CiWorkflowDiff::TouchAll + }; + select_affected_gates(dag, &diff) + } + fn topo_sort_subset( dag: &CiWorkflowDagInput, subset: &HashSet<&str>, @@ -997,6 +1028,47 @@ pub mod gunbc_ci { let wide = CiWorkflowDiff::TouchedGates(BTreeSet::from(["tests".into()])); assert!(selection_subset_under_touch_set_growth(&dag, &empty, &wide)); } + + #[test] + fn binary_shim_receipt_unknown_narrowing_matches_touch_all() { + let dag = demo_ci_dag(); + let receipt = CiBinaryShimAffectedSetReceipt { + narrowing_available: false, + proven_direct_gate_touches: BTreeSet::from(["lint".into()]), + }; + let got = select_affected_gates_for_binary_shim(&dag, &receipt).expect("valid dag"); + let expect = select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("valid dag"); + assert_eq!(got, expect); + } + + #[test] + fn binary_shim_receipt_narrow_lint_matches_touched_gates_lint() { + let dag = demo_ci_dag(); + let receipt = CiBinaryShimAffectedSetReceipt { + narrowing_available: true, + proven_direct_gate_touches: BTreeSet::from(["lint".into()]), + }; + let got = select_affected_gates_for_binary_shim(&dag, &receipt).expect("valid dag"); + let expect = select_affected_gates( + &dag, + &CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])), + ) + .expect("valid dag"); + assert_eq!(got, expect); + } + + #[test] + fn binary_shim_receipt_empty_narrow_seed_yields_empty_plan() { + let dag = demo_ci_dag(); + let receipt = CiBinaryShimAffectedSetReceipt { + narrowing_available: true, + proven_direct_gate_touches: BTreeSet::new(), + }; + assert_eq!( + select_affected_gates_for_binary_shim(&dag, &receipt), + Ok(Vec::new()) + ); + } } } diff --git a/src/v3/compiler/src/gunbc_ci.rs b/src/v3/compiler/src/gunbc_ci.rs new file mode 100644 index 00000000000..b3d259871e0 --- /dev/null +++ b/src/v3/compiler/src/gunbc_ci.rs @@ -0,0 +1,551 @@ +//! `gunbc.ci` workflow gate selection — affected-set dispatch substrate. +//! +//! **Authority** (Phase B): `docs/briefs/r3-wave1-s6-slice7-affected-set-impl-worker.md`; +//! fail-closed + obligation closure: `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3–§4; +//! carrier shape: `dsl/gunbc/ci.dag` (`CIWorkflowDag`, `CIGateEdge`: `from` is a prerequisite of `to`). +//! +//! This module is intentionally **pure**: every decision is a function of +//! [`CiWorkflowDagInput`] + [`CiWorkflowDiff`] only. Git diffs, path-regex, env, +//! and PR #2713 lens receipts are upstream responsibilities; they must be +//! mapped into [`CiWorkflowDiff`] (or a superset-equivalent touch set) before +//! calling [`select_affected_gates`]. **BinaryShim / gate #103:** the adapter +//! boundary [`CiBinaryShimAffectedSetReceipt`] + [`select_affected_gates_for_binary_shim`] +//! consumes PR #2713 lens output after upstream lowers affected nodes into gate ids. +//! +//! **Verifier ratchet witness (Phase C scaffolding)** — `docs/design-ci-workflow-substrate-shape-2026-05-12.md` +//! S5 + prequeue §5.2: monotone **inclusion** under enlarging the touched-id +//! set (smaller touch ⊆ larger touch ⇒ selected subset ⊆ selected superset). +//! See [`selection_subset_under_touch_set_growth`]. +//! +//! **Expansion semantics:** [`select_affected_gates`] walks each `CIGateEdge` **symmetrically** +//! (treat `from → to` as an undirected adjacency) to fixpoint, so a downstream obligation +//! pulls in its sibling prerequisites (e.g. touching `lint` selects `l1-ratchet`, which +//! forces `tests` because both parents are prerequisites of `l1-ratchet`). The returned +//! order is a **topological sort** of that vertex set on the directed prerequisite subgraph. +//! +//! **Global carrier validity:** before expanding any touch seed, [`select_affected_gates`] requires +//! the **entire** directed prerequisite graph on [`CiWorkflowDagInput::gates`] to be acyclic. A cycle +//! confined to an unselected weak component must still fail closed (P3), not succeed in “narrow” mode. + +use std::collections::{BTreeSet, HashMap, HashSet, VecDeque}; + +/// Structural mirror of `CIGate.id` in `dsl/gunbc/ci.dag`. +pub type CiGateId = String; + +/// Structural mirror of `CIGate` in `dsl/gunbc/ci.dag`. +/// +/// [`select_affected_gates`] consults only [`CiGateMeta::id`]. [`CiGateMeta::blocking`] is carried +/// for **carrier parity** with the DSL record so BinaryShim / dispatch wiring can map rows without +/// a forked struct; merge-blocking vs advisory semantics stay **outside** this selection substrate +/// until a consumer reads the field. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CiGateMeta { + pub id: CiGateId, + pub blocking: bool, +} + +/// Structural mirror of `CIWorkflowDag`: gate roster + prerequisite edges. +/// +/// Each edge `(from, to)` means **`from` must complete before `to`** (matches +/// `CIGateEdge { from, to }` in `ci.dag`). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct CiWorkflowDagInput { + pub gates: Vec, + /// Prerequisite edges: `from` runs before `to`. + pub edges: Vec<(CiGateId, CiGateId)>, +} + +// Practice 4 (coproduct checkpoint, `docs/modeling-discipline.md` §4): +// 🟢 GREEN — terminal upstream-policy boundary for gate-id touch semantics before +// `select_affected_gates`: `TouchAll` is the conservative in-DAG superset seed; +// `TouchedGates` is the explicit finite set upstream mapped from lens/git facts. +// Ledger: no third authority here — path-regex, env, and PR #2713 receipts stay +// outside this module (Slice 7 brief + affected-set selection canvas §1). +/// Diff-against-base input projected to **gate ids** (single authority at this boundary). +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CiWorkflowDiff { + /// Every gate id in [`CiWorkflowDagInput::gates`] is treated as directly touched. + TouchAll, + /// Upstream bundle names gate ids with proven or assumed direct impact. + TouchedGates(BTreeSet), +} + +// Practice 4 (coproduct checkpoint, `docs/modeling-discipline.md` §4): +// 🟢 GREEN — terminal typed failure surface for [`select_affected_gates`]: malformed `CIWorkflowDag` +// carrier vs non-acyclic directed prerequisites (full roster graph and/or selected subset; no silent success). +// Ledger: (1) fact placement — not split across consumers; this `Result` boundary owns the `Err` +// coproduct. (2) variant-is-data — rejected; payloads differ structurally. (3) algebraic — not +// `std/` refs. (4) dimensional — not one decomposed axis space; each variant is an irreducible +// outcome at the `dsl/gunbc/ci.dag` structural mirror until `.dag`-owned dispatch absorbs diagnostics. +/// [`select_affected_gates`] failed: malformed `CIWorkflowDag` carrier or non-acyclic prerequisites. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CiAffectedGatesError { + /// The same `CIGate.id` appears more than once in [`CiWorkflowDagInput::gates`] (malformed carrier; must not collapse into a `HashSet` silently). + DuplicateGateRosterId { id: CiGateId }, + /// `CIGateEdge` names a gate id not present in [`CiWorkflowDagInput::gates`] (violates single roster authority in `dsl/gunbc/ci.dag`). + UnknownEdgeEndpoint { from: CiGateId, to: CiGateId }, + /// The directed prerequisite graph is not a DAG: either the **full** roster graph from + /// [`CiWorkflowDagInput::edges`] contains a cycle, or the selected vertex set’s induced subgraph + /// does (broken indegree accounting). + NonAcyclicPrerequisiteGraph, +} + +/// On success, returns gate ids to execute: the **connected component** of the seed under the symmetric +/// closure of prerequisite edges (iterate `from ↔ to` adjacency to fixpoint), then +/// **topologically sorted** on the directed subgraph (prerequisites first). +/// +/// **Fail-closed** (`design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3): +/// any touched id not present in `dag.gates` forces **all** gates listed in `dag.gates` +/// into the plan (superset within this DAG). +/// +/// **Empty diff** (`TouchedGates` empty): returns `Ok` of an **empty** plan — callers map that +/// to “no merge-blocking re-verify” vs “full superset” per repository policy **outside** +/// this function (this module does not read policy carriers). +/// +/// **Malformed carrier:** duplicate [`CiGateMeta::id`] values in [`CiWorkflowDagInput::gates`] +/// yield [`CiAffectedGatesError::DuplicateGateRosterId`] (roster must be unique before any `HashSet` +/// view so gates are not dropped silently). Any edge whose `from` or `to` is absent from that roster +/// yields [`CiAffectedGatesError::UnknownEdgeEndpoint`]. Any directed cycle in the **full** +/// prerequisite graph on the roster (not only the touch-selected component) yields +/// [`CiAffectedGatesError::NonAcyclicPrerequisiteGraph`] instead of returning `Ok` in narrow mode. +pub fn select_affected_gates( + dag: &CiWorkflowDagInput, + diff: &CiWorkflowDiff, +) -> Result, CiAffectedGatesError> { + validate_unique_gate_roster(dag)?; + let known: HashSet<&str> = dag.gates.iter().map(|g| g.id.as_str()).collect(); + for (from, to) in &dag.edges { + if !known.contains(from.as_str()) || !known.contains(to.as_str()) { + return Err(CiAffectedGatesError::UnknownEdgeEndpoint { + from: from.clone(), + to: to.clone(), + }); + } + } + + // Fail-closed on the whole carrier: a cycle in an unselected component must not yield `Ok` + // for a disjoint touch seed (codex / P3). + topo_sort_subset(dag, &known)?; + + let force_superset = match diff { + CiWorkflowDiff::TouchAll => false, + CiWorkflowDiff::TouchedGates(ids) => ids.iter().any(|id| !known.contains(id.as_str())), + }; + + let seed: HashSet<&str> = if force_superset { + known.clone() + } else { + match diff { + CiWorkflowDiff::TouchAll => known.clone(), + CiWorkflowDiff::TouchedGates(ids) => ids.iter().map(|s| s.as_str()).collect(), + } + }; + + if seed.is_empty() { + return Ok(Vec::new()); + } + + // Symmetric edge closure: expand along `from ↔ to` until stable, then topo-sort + // the induced set on directed prerequisite edges. + let mut selected: HashSet<&str> = seed.clone(); + loop { + let before = selected.len(); + for (from, to) in &dag.edges { + let from = from.as_str(); + let to = to.as_str(); + if selected.contains(from) { + selected.insert(to); + } + if selected.contains(to) { + selected.insert(from); + } + } + if selected.len() == before { + break; + } + } + + topo_sort_subset(dag, &selected) +} + +/// Receipt boundary for **BinaryShim** gate dispatch after the PR #2713 affected-set +/// stack (and any obligation metadata) has been lowered into **`CIGate.id` strings** +/// from `dsl/gunbc/ci.dag` — not raw `NodeRef` keys (those are mapped upstream per +/// `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §1.4). +/// +/// When `narrowing_available` is `false`, selection fails closed to the full gate +/// roster ([`CiWorkflowDiff::TouchAll`]; canvas §3: unknown dimension / missing +/// receipt / unbuildable DAG pair). +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct CiBinaryShimAffectedSetReceipt { + /// `false` forces [`CiWorkflowDiff::TouchAll`] regardless of `proven_direct_gate_touches`. + pub narrowing_available: bool, + /// Gate ids the lens stack proved directly touched (seed before symmetric expansion). + pub proven_direct_gate_touches: BTreeSet, +} + +/// `BinaryShim` entry: map a post-lens receipt into [`select_affected_gates`]. +pub fn select_affected_gates_for_binary_shim( + dag: &CiWorkflowDagInput, + receipt: &CiBinaryShimAffectedSetReceipt, +) -> Result, CiAffectedGatesError> { + let diff = if receipt.narrowing_available { + CiWorkflowDiff::TouchedGates(receipt.proven_direct_gate_touches.clone()) + } else { + CiWorkflowDiff::TouchAll + }; + select_affected_gates(dag, &diff) +} + +fn validate_unique_gate_roster(dag: &CiWorkflowDagInput) -> Result<(), CiAffectedGatesError> { + let mut seen: HashSet<&str> = HashSet::new(); + for g in &dag.gates { + if !seen.insert(g.id.as_str()) { + return Err(CiAffectedGatesError::DuplicateGateRosterId { id: g.id.clone() }); + } + } + Ok(()) +} + +/// Set inclusion witness for ratchet tests. +/// +/// Returns whether `select_affected_gates(dag, narrow)` ⊆ `select_affected_gates(dag, wide)` as sets +/// when **both** calls succeed; if either returns `Err`, returns `false`. +/// Intended use: both arguments are [`CiWorkflowDiff::TouchedGates`] with `narrow_ids ⊆ wide_ids`, or +/// `wide` is [`CiWorkflowDiff::TouchAll`] (superset selection). Other pairings are not a refinement +/// partial order and may return `false` even when both plans are individually valid. +pub fn selection_subset_under_touch_set_growth( + dag: &CiWorkflowDagInput, + narrow: &CiWorkflowDiff, + wide: &CiWorkflowDiff, +) -> bool { + match ( + select_affected_gates(dag, narrow), + select_affected_gates(dag, wide), + ) { + (Ok(a), Ok(b)) => { + let a: BTreeSet<_> = a.into_iter().collect(); + let b: BTreeSet<_> = b.into_iter().collect(); + a.is_subset(&b) + } + _ => false, + } +} + +fn topo_sort_subset( + dag: &CiWorkflowDagInput, + subset: &HashSet<&str>, +) -> Result, CiAffectedGatesError> { + let mut indegree: HashMap<&str, usize> = HashMap::new(); + for id in subset.iter().copied() { + indegree.entry(id).or_insert(0); + } + for (from, to) in &dag.edges { + let from = from.as_str(); + let to = to.as_str(); + if subset.contains(from) && subset.contains(to) { + *indegree.entry(to).or_insert(0) += 1; + } + } + + let mut queue: VecDeque<&str> = indegree + .iter() + .filter(|(_, d)| **d == 0) + .map(|(n, _)| *n) + .collect::>() + .into_iter() + .collect(); + + let mut out: Vec = Vec::new(); + while let Some(n) = queue.pop_front() { + out.push(n.to_string()); + for (from, to) in &dag.edges { + if from.as_str() != n { + continue; + } + let to = to.as_str(); + if !subset.contains(to) { + continue; + } + if let Some(d) = indegree.get_mut(to) { + *d -= 1; + if *d == 0 { + queue.push_back(to); + } + } + } + } + + if out.len() != subset.len() { + return Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn demo_ci_dag() -> CiWorkflowDagInput { + CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "compile-gates".into(), + blocking: true, + }, + CiGateMeta { + id: "lint".into(), + blocking: true, + }, + CiGateMeta { + id: "tests".into(), + blocking: true, + }, + CiGateMeta { + id: "l1-ratchet".into(), + blocking: true, + }, + ], + edges: vec![ + ("compile-gates".into(), "lint".into()), + ("compile-gates".into(), "tests".into()), + ("lint".into(), "l1-ratchet".into()), + ("tests".into(), "l1-ratchet".into()), + ], + } + } + + #[test] + fn select_affected_gates_empty_diff_returns_empty_plan() { + let dag = demo_ci_dag(); + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::new()); + assert_eq!(select_affected_gates(&dag, &diff), Ok(Vec::new())); + } + + #[test] + fn select_affected_gates_touch_all_runs_full_dag_in_topo_order() { + let dag = demo_ci_dag(); + let got = + select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("demo dag is valid"); + assert_eq!( + got, + vec![ + "compile-gates".to_string(), + "lint".to_string(), + "tests".to_string(), + "l1-ratchet".to_string(), + ] + ); + } + + #[test] + fn select_affected_gates_isolated_gate_no_edges() { + let dag = CiWorkflowDagInput { + gates: vec![CiGateMeta { + id: "solo".into(), + blocking: true, + }], + edges: vec![], + }; + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["solo".into()])); + assert_eq!( + select_affected_gates(&dag, &diff), + Ok(vec!["solo".to_string()]) + ); + } + + #[test] + fn select_affected_gates_unknown_touch_id_forces_superset() { + let dag = demo_ci_dag(); + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["no-such-gate".into()])); + let got = select_affected_gates(&dag, &diff).expect("superset path is acyclic"); + let expect = select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("demo dag"); + assert_eq!(got, expect); + } + + #[test] + fn select_affected_gates_rejects_unknown_edge_endpoint() { + let dag = CiWorkflowDagInput { + gates: vec![CiGateMeta { + id: "solo".into(), + blocking: true, + }], + edges: vec![("solo".into(), "phantom".into())], + }; + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["solo".into()])); + assert_eq!( + select_affected_gates(&dag, &diff), + Err(CiAffectedGatesError::UnknownEdgeEndpoint { + from: "solo".into(), + to: "phantom".into(), + }) + ); + } + + #[test] + fn select_affected_gates_rejects_duplicate_gate_roster_ids() { + let dag = CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "dup".into(), + blocking: true, + }, + CiGateMeta { + id: "dup".into(), + blocking: false, + }, + ], + edges: vec![], + }; + assert_eq!( + select_affected_gates(&dag, &CiWorkflowDiff::TouchAll), + Err(CiAffectedGatesError::DuplicateGateRosterId { id: "dup".into() }) + ); + } + + #[test] + fn select_affected_gates_rejects_prerequisite_cycle() { + let dag = CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "a".into(), + blocking: true, + }, + CiGateMeta { + id: "b".into(), + blocking: true, + }, + ], + edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], + }; + assert_eq!( + select_affected_gates(&dag, &CiWorkflowDiff::TouchAll), + Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) + ); + } + + #[test] + fn select_affected_gates_rejects_global_cycle_even_when_touch_isolated() { + let dag = CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "a".into(), + blocking: true, + }, + CiGateMeta { + id: "b".into(), + blocking: true, + }, + CiGateMeta { + id: "c".into(), + blocking: true, + }, + ], + edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], + }; + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["c".into()])); + assert_eq!( + select_affected_gates(&dag, &diff), + Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) + ); + } + + #[test] + fn select_affected_gates_empty_touch_rejects_when_global_graph_has_cycle() { + let dag = CiWorkflowDagInput { + gates: vec![ + CiGateMeta { + id: "a".into(), + blocking: true, + }, + CiGateMeta { + id: "b".into(), + blocking: true, + }, + ], + edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], + }; + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::new()); + assert_eq!( + select_affected_gates(&dag, &diff), + Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) + ); + } + + #[test] + fn select_affected_gates_lint_only_pulls_l1_and_shared_prereqs() { + let dag = demo_ci_dag(); + let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])); + let got = select_affected_gates(&dag, &diff).expect("demo dag"); + let set: BTreeSet<_> = got.iter().cloned().collect(); + assert_eq!( + set, + BTreeSet::from([ + "compile-gates".into(), + "lint".into(), + "tests".into(), + "l1-ratchet".into(), + ]) + ); + assert_eq!(got.first().map(String::as_str), Some("compile-gates")); + } + + #[test] + fn selection_subset_under_touch_set_growth_holds_on_demo_dag() { + let dag = demo_ci_dag(); + let narrow = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])); + let wide = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into(), "tests".into()])); + assert!(selection_subset_under_touch_set_growth( + &dag, &narrow, &wide + )); + } + + #[test] + fn selection_subset_empty_diff_is_subset_of_nonempty() { + let dag = demo_ci_dag(); + let empty = CiWorkflowDiff::TouchedGates(BTreeSet::new()); + let wide = CiWorkflowDiff::TouchedGates(BTreeSet::from(["tests".into()])); + assert!(selection_subset_under_touch_set_growth(&dag, &empty, &wide)); + } + + #[test] + fn binary_shim_receipt_unknown_narrowing_matches_touch_all() { + let dag = demo_ci_dag(); + let receipt = CiBinaryShimAffectedSetReceipt { + narrowing_available: false, + proven_direct_gate_touches: BTreeSet::from(["lint".into()]), + }; + let got = select_affected_gates_for_binary_shim(&dag, &receipt).expect("valid dag"); + let expect = select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("valid dag"); + assert_eq!(got, expect); + } + + #[test] + fn binary_shim_receipt_narrow_lint_matches_touched_gates_lint() { + let dag = demo_ci_dag(); + let receipt = CiBinaryShimAffectedSetReceipt { + narrowing_available: true, + proven_direct_gate_touches: BTreeSet::from(["lint".into()]), + }; + let got = select_affected_gates_for_binary_shim(&dag, &receipt).expect("valid dag"); + let expect = select_affected_gates( + &dag, + &CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])), + ) + .expect("valid dag"); + assert_eq!(got, expect); + } + + #[test] + fn binary_shim_receipt_empty_narrow_seed_yields_empty_plan() { + let dag = demo_ci_dag(); + let receipt = CiBinaryShimAffectedSetReceipt { + narrowing_available: true, + proven_direct_gate_touches: BTreeSet::new(), + }; + assert_eq!( + select_affected_gates_for_binary_shim(&dag, &receipt), + Ok(Vec::new()) + ); + } +} diff --git a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs index ab49ec33b40..06d2ba3a3fe 100644 --- a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs +++ b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs @@ -42,7 +42,7 @@ //! surface; dissolution target is `.dag` `TestClaim` data per `sg0_census_test.rs` R1C-E notes. //! This crate fails to build if the cited worker brief is removed from the worktree. -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::sync::OnceLock; use crate::common::find_list_empty_constructor_tag; @@ -54,7 +54,8 @@ use v3_compiler::evaluator::{ evaluate_body, EvalFrame, EvalStateStack, EvalStrategy, InputEvaluationOrder, NamedField, Value, }; use v3_compiler::gunbc_ci::{ - select_affected_gates, CiGateMeta, CiWorkflowDagInput, CiWorkflowDiff, + select_affected_gates, select_affected_gates_for_binary_shim, CiBinaryShimAffectedSetReceipt, + CiGateMeta, CiWorkflowDagInput, CiWorkflowDiff, }; use v3_compiler::lens_cost::complexity_of; use v3_compiler::{ @@ -944,6 +945,63 @@ fn lens_self_application_demonstrated_ci_touch_all_affected_gates_order() { ); } +// --- R3 gate #103 (`ci_uses_affected_set_selection`): BinaryShim consumes lens-shaped receipts. + +#[test] +fn ci_uses_affected_set_selection_binary_shim_narrow_on_gunbc_ci_topology() { + let g = gate57_ci_artifacts(); + let receipt = CiBinaryShimAffectedSetReceipt { + narrowing_available: true, + proven_direct_gate_touches: BTreeSet::from([String::from("tests")]), + }; + let plan = select_affected_gates_for_binary_shim(&g.input, &receipt) + .expect("binary shim selection must succeed on gunbc-ci topology"); + let touch = select_affected_gates( + &g.input, + &CiWorkflowDiff::TouchedGates(BTreeSet::from([String::from("tests")])), + ) + .expect("baseline touched-gates selection"); + assert_eq!(plan, touch); +} + +#[test] +fn ci_uses_affected_set_selection_binary_shim_unknown_receipt_full_roster() { + let g = gate57_ci_artifacts(); + let receipt = CiBinaryShimAffectedSetReceipt { + narrowing_available: false, + proven_direct_gate_touches: BTreeSet::from([String::from("tests")]), + }; + let plan = select_affected_gates_for_binary_shim(&g.input, &receipt) + .expect("binary shim selection must succeed on gunbc-ci topology"); + let full = select_affected_gates(&g.input, &CiWorkflowDiff::TouchAll).expect("TouchAll baseline"); + assert_eq!(plan, full); +} + +#[test] +fn workflow_no_path_regex_policy_ci_yml() { + use std::fs; + use std::path::Path; + + let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../.."); + let repo_root = repo_root + .canonicalize() + .unwrap_or_else(|e| panic!("canonicalize repo root {}: {e}", repo_root.display())); + let path = repo_root.join(".github/workflows/ci.yml"); + let raw = fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())); + + for forbidden in [ + "git diff --name-only", + "needs.changes.outputs", + "grep -vE '^(docs/.*|[^/]+\\.md)$'", + ] { + assert!( + !raw.contains(forbidden), + "{} must not contain Layer-2 selection fingerprint `{forbidden}` (gate ci_uses_affected_set_selection)", + path.display() + ); + } +} + #[test] fn lens_self_application_demonstrated_ci_prereq_fanout_from_carrier() { let g = gate57_ci_artifacts(); From eba633632abdfca43ccd117e78018d37dc4b3b92 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 16:36:51 -0400 Subject: [PATCH 02/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/v3/compiler/src/gunbc_ci.rs | 551 -------------------------------- 1 file changed, 551 deletions(-) delete mode 100644 src/v3/compiler/src/gunbc_ci.rs diff --git a/src/v3/compiler/src/gunbc_ci.rs b/src/v3/compiler/src/gunbc_ci.rs deleted file mode 100644 index b3d259871e0..00000000000 --- a/src/v3/compiler/src/gunbc_ci.rs +++ /dev/null @@ -1,551 +0,0 @@ -//! `gunbc.ci` workflow gate selection — affected-set dispatch substrate. -//! -//! **Authority** (Phase B): `docs/briefs/r3-wave1-s6-slice7-affected-set-impl-worker.md`; -//! fail-closed + obligation closure: `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3–§4; -//! carrier shape: `dsl/gunbc/ci.dag` (`CIWorkflowDag`, `CIGateEdge`: `from` is a prerequisite of `to`). -//! -//! This module is intentionally **pure**: every decision is a function of -//! [`CiWorkflowDagInput`] + [`CiWorkflowDiff`] only. Git diffs, path-regex, env, -//! and PR #2713 lens receipts are upstream responsibilities; they must be -//! mapped into [`CiWorkflowDiff`] (or a superset-equivalent touch set) before -//! calling [`select_affected_gates`]. **BinaryShim / gate #103:** the adapter -//! boundary [`CiBinaryShimAffectedSetReceipt`] + [`select_affected_gates_for_binary_shim`] -//! consumes PR #2713 lens output after upstream lowers affected nodes into gate ids. -//! -//! **Verifier ratchet witness (Phase C scaffolding)** — `docs/design-ci-workflow-substrate-shape-2026-05-12.md` -//! S5 + prequeue §5.2: monotone **inclusion** under enlarging the touched-id -//! set (smaller touch ⊆ larger touch ⇒ selected subset ⊆ selected superset). -//! See [`selection_subset_under_touch_set_growth`]. -//! -//! **Expansion semantics:** [`select_affected_gates`] walks each `CIGateEdge` **symmetrically** -//! (treat `from → to` as an undirected adjacency) to fixpoint, so a downstream obligation -//! pulls in its sibling prerequisites (e.g. touching `lint` selects `l1-ratchet`, which -//! forces `tests` because both parents are prerequisites of `l1-ratchet`). The returned -//! order is a **topological sort** of that vertex set on the directed prerequisite subgraph. -//! -//! **Global carrier validity:** before expanding any touch seed, [`select_affected_gates`] requires -//! the **entire** directed prerequisite graph on [`CiWorkflowDagInput::gates`] to be acyclic. A cycle -//! confined to an unselected weak component must still fail closed (P3), not succeed in “narrow” mode. - -use std::collections::{BTreeSet, HashMap, HashSet, VecDeque}; - -/// Structural mirror of `CIGate.id` in `dsl/gunbc/ci.dag`. -pub type CiGateId = String; - -/// Structural mirror of `CIGate` in `dsl/gunbc/ci.dag`. -/// -/// [`select_affected_gates`] consults only [`CiGateMeta::id`]. [`CiGateMeta::blocking`] is carried -/// for **carrier parity** with the DSL record so BinaryShim / dispatch wiring can map rows without -/// a forked struct; merge-blocking vs advisory semantics stay **outside** this selection substrate -/// until a consumer reads the field. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CiGateMeta { - pub id: CiGateId, - pub blocking: bool, -} - -/// Structural mirror of `CIWorkflowDag`: gate roster + prerequisite edges. -/// -/// Each edge `(from, to)` means **`from` must complete before `to`** (matches -/// `CIGateEdge { from, to }` in `ci.dag`). -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct CiWorkflowDagInput { - pub gates: Vec, - /// Prerequisite edges: `from` runs before `to`. - pub edges: Vec<(CiGateId, CiGateId)>, -} - -// Practice 4 (coproduct checkpoint, `docs/modeling-discipline.md` §4): -// 🟢 GREEN — terminal upstream-policy boundary for gate-id touch semantics before -// `select_affected_gates`: `TouchAll` is the conservative in-DAG superset seed; -// `TouchedGates` is the explicit finite set upstream mapped from lens/git facts. -// Ledger: no third authority here — path-regex, env, and PR #2713 receipts stay -// outside this module (Slice 7 brief + affected-set selection canvas §1). -/// Diff-against-base input projected to **gate ids** (single authority at this boundary). -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CiWorkflowDiff { - /// Every gate id in [`CiWorkflowDagInput::gates`] is treated as directly touched. - TouchAll, - /// Upstream bundle names gate ids with proven or assumed direct impact. - TouchedGates(BTreeSet), -} - -// Practice 4 (coproduct checkpoint, `docs/modeling-discipline.md` §4): -// 🟢 GREEN — terminal typed failure surface for [`select_affected_gates`]: malformed `CIWorkflowDag` -// carrier vs non-acyclic directed prerequisites (full roster graph and/or selected subset; no silent success). -// Ledger: (1) fact placement — not split across consumers; this `Result` boundary owns the `Err` -// coproduct. (2) variant-is-data — rejected; payloads differ structurally. (3) algebraic — not -// `std/` refs. (4) dimensional — not one decomposed axis space; each variant is an irreducible -// outcome at the `dsl/gunbc/ci.dag` structural mirror until `.dag`-owned dispatch absorbs diagnostics. -/// [`select_affected_gates`] failed: malformed `CIWorkflowDag` carrier or non-acyclic prerequisites. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CiAffectedGatesError { - /// The same `CIGate.id` appears more than once in [`CiWorkflowDagInput::gates`] (malformed carrier; must not collapse into a `HashSet` silently). - DuplicateGateRosterId { id: CiGateId }, - /// `CIGateEdge` names a gate id not present in [`CiWorkflowDagInput::gates`] (violates single roster authority in `dsl/gunbc/ci.dag`). - UnknownEdgeEndpoint { from: CiGateId, to: CiGateId }, - /// The directed prerequisite graph is not a DAG: either the **full** roster graph from - /// [`CiWorkflowDagInput::edges`] contains a cycle, or the selected vertex set’s induced subgraph - /// does (broken indegree accounting). - NonAcyclicPrerequisiteGraph, -} - -/// On success, returns gate ids to execute: the **connected component** of the seed under the symmetric -/// closure of prerequisite edges (iterate `from ↔ to` adjacency to fixpoint), then -/// **topologically sorted** on the directed subgraph (prerequisites first). -/// -/// **Fail-closed** (`design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3): -/// any touched id not present in `dag.gates` forces **all** gates listed in `dag.gates` -/// into the plan (superset within this DAG). -/// -/// **Empty diff** (`TouchedGates` empty): returns `Ok` of an **empty** plan — callers map that -/// to “no merge-blocking re-verify” vs “full superset” per repository policy **outside** -/// this function (this module does not read policy carriers). -/// -/// **Malformed carrier:** duplicate [`CiGateMeta::id`] values in [`CiWorkflowDagInput::gates`] -/// yield [`CiAffectedGatesError::DuplicateGateRosterId`] (roster must be unique before any `HashSet` -/// view so gates are not dropped silently). Any edge whose `from` or `to` is absent from that roster -/// yields [`CiAffectedGatesError::UnknownEdgeEndpoint`]. Any directed cycle in the **full** -/// prerequisite graph on the roster (not only the touch-selected component) yields -/// [`CiAffectedGatesError::NonAcyclicPrerequisiteGraph`] instead of returning `Ok` in narrow mode. -pub fn select_affected_gates( - dag: &CiWorkflowDagInput, - diff: &CiWorkflowDiff, -) -> Result, CiAffectedGatesError> { - validate_unique_gate_roster(dag)?; - let known: HashSet<&str> = dag.gates.iter().map(|g| g.id.as_str()).collect(); - for (from, to) in &dag.edges { - if !known.contains(from.as_str()) || !known.contains(to.as_str()) { - return Err(CiAffectedGatesError::UnknownEdgeEndpoint { - from: from.clone(), - to: to.clone(), - }); - } - } - - // Fail-closed on the whole carrier: a cycle in an unselected component must not yield `Ok` - // for a disjoint touch seed (codex / P3). - topo_sort_subset(dag, &known)?; - - let force_superset = match diff { - CiWorkflowDiff::TouchAll => false, - CiWorkflowDiff::TouchedGates(ids) => ids.iter().any(|id| !known.contains(id.as_str())), - }; - - let seed: HashSet<&str> = if force_superset { - known.clone() - } else { - match diff { - CiWorkflowDiff::TouchAll => known.clone(), - CiWorkflowDiff::TouchedGates(ids) => ids.iter().map(|s| s.as_str()).collect(), - } - }; - - if seed.is_empty() { - return Ok(Vec::new()); - } - - // Symmetric edge closure: expand along `from ↔ to` until stable, then topo-sort - // the induced set on directed prerequisite edges. - let mut selected: HashSet<&str> = seed.clone(); - loop { - let before = selected.len(); - for (from, to) in &dag.edges { - let from = from.as_str(); - let to = to.as_str(); - if selected.contains(from) { - selected.insert(to); - } - if selected.contains(to) { - selected.insert(from); - } - } - if selected.len() == before { - break; - } - } - - topo_sort_subset(dag, &selected) -} - -/// Receipt boundary for **BinaryShim** gate dispatch after the PR #2713 affected-set -/// stack (and any obligation metadata) has been lowered into **`CIGate.id` strings** -/// from `dsl/gunbc/ci.dag` — not raw `NodeRef` keys (those are mapped upstream per -/// `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §1.4). -/// -/// When `narrowing_available` is `false`, selection fails closed to the full gate -/// roster ([`CiWorkflowDiff::TouchAll`]; canvas §3: unknown dimension / missing -/// receipt / unbuildable DAG pair). -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct CiBinaryShimAffectedSetReceipt { - /// `false` forces [`CiWorkflowDiff::TouchAll`] regardless of `proven_direct_gate_touches`. - pub narrowing_available: bool, - /// Gate ids the lens stack proved directly touched (seed before symmetric expansion). - pub proven_direct_gate_touches: BTreeSet, -} - -/// `BinaryShim` entry: map a post-lens receipt into [`select_affected_gates`]. -pub fn select_affected_gates_for_binary_shim( - dag: &CiWorkflowDagInput, - receipt: &CiBinaryShimAffectedSetReceipt, -) -> Result, CiAffectedGatesError> { - let diff = if receipt.narrowing_available { - CiWorkflowDiff::TouchedGates(receipt.proven_direct_gate_touches.clone()) - } else { - CiWorkflowDiff::TouchAll - }; - select_affected_gates(dag, &diff) -} - -fn validate_unique_gate_roster(dag: &CiWorkflowDagInput) -> Result<(), CiAffectedGatesError> { - let mut seen: HashSet<&str> = HashSet::new(); - for g in &dag.gates { - if !seen.insert(g.id.as_str()) { - return Err(CiAffectedGatesError::DuplicateGateRosterId { id: g.id.clone() }); - } - } - Ok(()) -} - -/// Set inclusion witness for ratchet tests. -/// -/// Returns whether `select_affected_gates(dag, narrow)` ⊆ `select_affected_gates(dag, wide)` as sets -/// when **both** calls succeed; if either returns `Err`, returns `false`. -/// Intended use: both arguments are [`CiWorkflowDiff::TouchedGates`] with `narrow_ids ⊆ wide_ids`, or -/// `wide` is [`CiWorkflowDiff::TouchAll`] (superset selection). Other pairings are not a refinement -/// partial order and may return `false` even when both plans are individually valid. -pub fn selection_subset_under_touch_set_growth( - dag: &CiWorkflowDagInput, - narrow: &CiWorkflowDiff, - wide: &CiWorkflowDiff, -) -> bool { - match ( - select_affected_gates(dag, narrow), - select_affected_gates(dag, wide), - ) { - (Ok(a), Ok(b)) => { - let a: BTreeSet<_> = a.into_iter().collect(); - let b: BTreeSet<_> = b.into_iter().collect(); - a.is_subset(&b) - } - _ => false, - } -} - -fn topo_sort_subset( - dag: &CiWorkflowDagInput, - subset: &HashSet<&str>, -) -> Result, CiAffectedGatesError> { - let mut indegree: HashMap<&str, usize> = HashMap::new(); - for id in subset.iter().copied() { - indegree.entry(id).or_insert(0); - } - for (from, to) in &dag.edges { - let from = from.as_str(); - let to = to.as_str(); - if subset.contains(from) && subset.contains(to) { - *indegree.entry(to).or_insert(0) += 1; - } - } - - let mut queue: VecDeque<&str> = indegree - .iter() - .filter(|(_, d)| **d == 0) - .map(|(n, _)| *n) - .collect::>() - .into_iter() - .collect(); - - let mut out: Vec = Vec::new(); - while let Some(n) = queue.pop_front() { - out.push(n.to_string()); - for (from, to) in &dag.edges { - if from.as_str() != n { - continue; - } - let to = to.as_str(); - if !subset.contains(to) { - continue; - } - if let Some(d) = indegree.get_mut(to) { - *d -= 1; - if *d == 0 { - queue.push_back(to); - } - } - } - } - - if out.len() != subset.len() { - return Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph); - } - Ok(out) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn demo_ci_dag() -> CiWorkflowDagInput { - CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "compile-gates".into(), - blocking: true, - }, - CiGateMeta { - id: "lint".into(), - blocking: true, - }, - CiGateMeta { - id: "tests".into(), - blocking: true, - }, - CiGateMeta { - id: "l1-ratchet".into(), - blocking: true, - }, - ], - edges: vec![ - ("compile-gates".into(), "lint".into()), - ("compile-gates".into(), "tests".into()), - ("lint".into(), "l1-ratchet".into()), - ("tests".into(), "l1-ratchet".into()), - ], - } - } - - #[test] - fn select_affected_gates_empty_diff_returns_empty_plan() { - let dag = demo_ci_dag(); - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::new()); - assert_eq!(select_affected_gates(&dag, &diff), Ok(Vec::new())); - } - - #[test] - fn select_affected_gates_touch_all_runs_full_dag_in_topo_order() { - let dag = demo_ci_dag(); - let got = - select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("demo dag is valid"); - assert_eq!( - got, - vec![ - "compile-gates".to_string(), - "lint".to_string(), - "tests".to_string(), - "l1-ratchet".to_string(), - ] - ); - } - - #[test] - fn select_affected_gates_isolated_gate_no_edges() { - let dag = CiWorkflowDagInput { - gates: vec![CiGateMeta { - id: "solo".into(), - blocking: true, - }], - edges: vec![], - }; - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["solo".into()])); - assert_eq!( - select_affected_gates(&dag, &diff), - Ok(vec!["solo".to_string()]) - ); - } - - #[test] - fn select_affected_gates_unknown_touch_id_forces_superset() { - let dag = demo_ci_dag(); - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["no-such-gate".into()])); - let got = select_affected_gates(&dag, &diff).expect("superset path is acyclic"); - let expect = select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("demo dag"); - assert_eq!(got, expect); - } - - #[test] - fn select_affected_gates_rejects_unknown_edge_endpoint() { - let dag = CiWorkflowDagInput { - gates: vec![CiGateMeta { - id: "solo".into(), - blocking: true, - }], - edges: vec![("solo".into(), "phantom".into())], - }; - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["solo".into()])); - assert_eq!( - select_affected_gates(&dag, &diff), - Err(CiAffectedGatesError::UnknownEdgeEndpoint { - from: "solo".into(), - to: "phantom".into(), - }) - ); - } - - #[test] - fn select_affected_gates_rejects_duplicate_gate_roster_ids() { - let dag = CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "dup".into(), - blocking: true, - }, - CiGateMeta { - id: "dup".into(), - blocking: false, - }, - ], - edges: vec![], - }; - assert_eq!( - select_affected_gates(&dag, &CiWorkflowDiff::TouchAll), - Err(CiAffectedGatesError::DuplicateGateRosterId { id: "dup".into() }) - ); - } - - #[test] - fn select_affected_gates_rejects_prerequisite_cycle() { - let dag = CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "a".into(), - blocking: true, - }, - CiGateMeta { - id: "b".into(), - blocking: true, - }, - ], - edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], - }; - assert_eq!( - select_affected_gates(&dag, &CiWorkflowDiff::TouchAll), - Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) - ); - } - - #[test] - fn select_affected_gates_rejects_global_cycle_even_when_touch_isolated() { - let dag = CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "a".into(), - blocking: true, - }, - CiGateMeta { - id: "b".into(), - blocking: true, - }, - CiGateMeta { - id: "c".into(), - blocking: true, - }, - ], - edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], - }; - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["c".into()])); - assert_eq!( - select_affected_gates(&dag, &diff), - Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) - ); - } - - #[test] - fn select_affected_gates_empty_touch_rejects_when_global_graph_has_cycle() { - let dag = CiWorkflowDagInput { - gates: vec![ - CiGateMeta { - id: "a".into(), - blocking: true, - }, - CiGateMeta { - id: "b".into(), - blocking: true, - }, - ], - edges: vec![("a".into(), "b".into()), ("b".into(), "a".into())], - }; - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::new()); - assert_eq!( - select_affected_gates(&dag, &diff), - Err(CiAffectedGatesError::NonAcyclicPrerequisiteGraph) - ); - } - - #[test] - fn select_affected_gates_lint_only_pulls_l1_and_shared_prereqs() { - let dag = demo_ci_dag(); - let diff = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])); - let got = select_affected_gates(&dag, &diff).expect("demo dag"); - let set: BTreeSet<_> = got.iter().cloned().collect(); - assert_eq!( - set, - BTreeSet::from([ - "compile-gates".into(), - "lint".into(), - "tests".into(), - "l1-ratchet".into(), - ]) - ); - assert_eq!(got.first().map(String::as_str), Some("compile-gates")); - } - - #[test] - fn selection_subset_under_touch_set_growth_holds_on_demo_dag() { - let dag = demo_ci_dag(); - let narrow = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])); - let wide = CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into(), "tests".into()])); - assert!(selection_subset_under_touch_set_growth( - &dag, &narrow, &wide - )); - } - - #[test] - fn selection_subset_empty_diff_is_subset_of_nonempty() { - let dag = demo_ci_dag(); - let empty = CiWorkflowDiff::TouchedGates(BTreeSet::new()); - let wide = CiWorkflowDiff::TouchedGates(BTreeSet::from(["tests".into()])); - assert!(selection_subset_under_touch_set_growth(&dag, &empty, &wide)); - } - - #[test] - fn binary_shim_receipt_unknown_narrowing_matches_touch_all() { - let dag = demo_ci_dag(); - let receipt = CiBinaryShimAffectedSetReceipt { - narrowing_available: false, - proven_direct_gate_touches: BTreeSet::from(["lint".into()]), - }; - let got = select_affected_gates_for_binary_shim(&dag, &receipt).expect("valid dag"); - let expect = select_affected_gates(&dag, &CiWorkflowDiff::TouchAll).expect("valid dag"); - assert_eq!(got, expect); - } - - #[test] - fn binary_shim_receipt_narrow_lint_matches_touched_gates_lint() { - let dag = demo_ci_dag(); - let receipt = CiBinaryShimAffectedSetReceipt { - narrowing_available: true, - proven_direct_gate_touches: BTreeSet::from(["lint".into()]), - }; - let got = select_affected_gates_for_binary_shim(&dag, &receipt).expect("valid dag"); - let expect = select_affected_gates( - &dag, - &CiWorkflowDiff::TouchedGates(BTreeSet::from(["lint".into()])), - ) - .expect("valid dag"); - assert_eq!(got, expect); - } - - #[test] - fn binary_shim_receipt_empty_narrow_seed_yields_empty_plan() { - let dag = demo_ci_dag(); - let receipt = CiBinaryShimAffectedSetReceipt { - narrowing_available: true, - proven_direct_gate_touches: BTreeSet::new(), - }; - assert_eq!( - select_affected_gates_for_binary_shim(&dag, &receipt), - Ok(Vec::new()) - ); - } -} From f10c13e3de16d04c5d64b7b6c8c891e9e363debb Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 16:42:42 -0400 Subject: [PATCH 03/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dsl/gunbc/ci_github_actions_workflow.dag | 40 +++++++++++----------- tools/gen_gunbc_ci_workflow_dag/src/lib.rs | 8 ++--- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/dsl/gunbc/ci_github_actions_workflow.dag b/dsl/gunbc/ci_github_actions_workflow.dag index 26e54b03d1a..6ba6ef69e73 100644 --- a/dsl/gunbc/ci_github_actions_workflow.dag +++ b/dsl/gunbc/ci_github_actions_workflow.dag @@ -15,7 +15,7 @@ import extdeps.github.actions { ShellType, Bash } -data gunbc_ci_github_actions_workflow: Workflow = { +data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: "ci", on: [Push { branches: ["main"], paths: [] }, PullRequest { branches: ["main"], types: [Opened, Synchronize, Reopened, ReadyForReview] }], concurrency: Some { @@ -25,14 +25,14 @@ data gunbc_ci_github_actions_workflow: Workflow = { explicit_single: none } }, - jobs: [{ + jobs: [Job { id: "fmt", name: none, runner: RunsOnExpression { expression: "${{ vars.CI_RUNNER || 'ubuntu-latest' }}" }, steps: [UsesStep { name: Some { value: "Checkout" }, id: none, - uses: { owner: "actions", repo: "checkout", ref: "v4" }, + uses: ActionRef { owner: "actions", repo: "checkout", ref: "v4" }, with: Some { value: { "fetch-depth": "1" } }, env: none, if_condition: none, @@ -41,7 +41,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { }, UsesStep { name: Some { value: "Setup Rust" }, id: none, - uses: { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, + uses: ActionRef { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, with: Some { value: { "components": "rustfmt", "cache": "false", "rustflags": "" } }, env: none, if_condition: none, @@ -66,14 +66,14 @@ data gunbc_ci_github_actions_workflow: Workflow = { timeout_minutes: Some { value: 5 }, continue_on_error: false, concurrency: none - }, { + }, Job { id: "ci", name: none, runner: RunsOnExpression { expression: "${{ vars.CI_RUNNER || 'ubuntu-latest' }}" }, steps: [UsesStep { name: Some { value: "Checkout" }, id: none, - uses: { owner: "actions", repo: "checkout", ref: "v4" }, + uses: ActionRef { owner: "actions", repo: "checkout", ref: "v4" }, with: Some { value: { "fetch-depth": "0" } }, env: none, if_condition: none, @@ -202,7 +202,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { }, UsesStep { name: Some { value: "Setup Rust" }, id: none, - uses: { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, + uses: ActionRef { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, with: Some { value: { "components": "rustfmt", "cache": "false", "rustflags": "" } }, env: none, if_condition: none, @@ -211,7 +211,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { }, UsesStep { name: Some { value: "Cache Cargo (ci bootstrap gate)" }, id: none, - uses: { owner: "actions", repo: "cache", ref: "v4" }, + uses: ActionRef { owner: "actions", repo: "cache", ref: "v4" }, with: Some { value: { "path": "~/.cargo/registry/index/\n~/.cargo/registry/cache/\n~/.cargo/git/db/\ntarget/\n", "key": "cargo-ci-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('src/v3/compiler/src/**', 'src/v3/compiler/tests/**') }}", "restore-keys": "cargo-ci-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-\ncargo-ci-${{ runner.os }}-\n" } }, env: none, if_condition: none, @@ -236,14 +236,14 @@ data gunbc_ci_github_actions_workflow: Workflow = { timeout_minutes: Some { value: 25 }, continue_on_error: false, concurrency: none - }, { + }, Job { id: "v3", name: none, runner: SelfHosted { labels: ["ubicloud-standard-8"] }, steps: [UsesStep { name: Some { value: "Checkout" }, id: none, - uses: { owner: "actions", repo: "checkout", ref: "v4" }, + uses: ActionRef { owner: "actions", repo: "checkout", ref: "v4" }, with: Some { value: { "fetch-depth": "1" } }, env: none, if_condition: none, @@ -252,7 +252,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { }, UsesStep { name: Some { value: "Setup Rust" }, id: none, - uses: { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, + uses: ActionRef { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, with: Some { value: { "components": "rustfmt, clippy", "cache": "false", "rustflags": "" } }, env: none, if_condition: none, @@ -261,7 +261,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { }, UsesStep { name: Some { value: "Setup Zig C toolchain" }, id: none, - uses: { owner: "goto-bus-stop", repo: "setup-zig", ref: "v2" }, + uses: ActionRef { owner: "goto-bus-stop", repo: "setup-zig", ref: "v2" }, with: Some { value: { "version": "0.13.0", "cache": "false" } }, env: none, if_condition: none, @@ -280,7 +280,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { }, UsesStep { name: Some { value: "Setup Python" }, id: none, - uses: { owner: "actions", repo: "setup-python", ref: "v6" }, + uses: ActionRef { owner: "actions", repo: "setup-python", ref: "v6" }, with: Some { value: { "python-version": "3.x" } }, env: none, if_condition: none, @@ -289,7 +289,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { }, UsesStep { name: Some { value: "Setup Go" }, id: none, - uses: { owner: "actions", repo: "setup-go", ref: "v6" }, + uses: ActionRef { owner: "actions", repo: "setup-go", ref: "v6" }, with: Some { value: { "go-version": "stable" } }, env: none, if_condition: none, @@ -298,7 +298,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { }, UsesStep { name: Some { value: "Cache Cargo (v3)" }, id: none, - uses: { owner: "actions", repo: "cache", ref: "v4" }, + uses: ActionRef { owner: "actions", repo: "cache", ref: "v4" }, with: Some { value: { "path": "~/.cargo/registry/index/\n~/.cargo/registry/cache/\n~/.cargo/git/db/\ntarget/\n", "key": "cargo-v3-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('src/v3/compiler/src/**', 'src/v3/compiler/tests/**') }}", "restore-keys": "cargo-v3-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-\ncargo-v3-${{ runner.os }}-\n" } }, env: none, if_condition: none, @@ -483,7 +483,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { timeout_minutes: Some { value: 120 }, continue_on_error: false, concurrency: none - }, { + }, Job { id: "self_host_ratchet", name: none, runner: SelfHosted { labels: ["ubicloud-standard-8"] }, @@ -500,7 +500,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { }, UsesStep { name: Some { value: "Checkout" }, id: none, - uses: { owner: "actions", repo: "checkout", ref: "v4" }, + uses: ActionRef { owner: "actions", repo: "checkout", ref: "v4" }, with: Some { value: { "fetch-depth": "1" } }, env: none, if_condition: Some { value: "github.event_name == 'push' && github.ref == 'refs/heads/main'" }, @@ -519,7 +519,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { }, UsesStep { name: Some { value: "Setup Rust" }, id: none, - uses: { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, + uses: ActionRef { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, with: Some { value: { "cache": "false", "rustflags": "" } }, env: none, if_condition: Some { value: "github.event_name == 'push' && github.ref == 'refs/heads/main'" }, @@ -528,7 +528,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { }, UsesStep { name: Some { value: "Cache Cargo (self_host_ratchet)" }, id: none, - uses: { owner: "actions", repo: "cache", ref: "v4" }, + uses: ActionRef { owner: "actions", repo: "cache", ref: "v4" }, with: Some { value: { "path": "~/.cargo/registry/index/\n~/.cargo/registry/cache/\n~/.cargo/git/db/\ntarget/\n", "key": "cargo-self-host-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('src/v3/compiler/**') }}", "restore-keys": "cargo-self-host-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-\ncargo-self-host-${{ runner.os }}-\n" } }, env: none, if_condition: Some { value: "github.event_name == 'push' && github.ref == 'refs/heads/main'" }, @@ -576,7 +576,7 @@ data gunbc_ci_github_actions_workflow: Workflow = { }], env: Some { value: { "CARGO_TERM_COLOR": "always", "RUSTFLAGS": "-D warnings" } }, permissions: Some { - value: { + value: WorkflowPermissions { contents: PermRead, pull_requests: PermRead, issues: PermNone, diff --git a/tools/gen_gunbc_ci_workflow_dag/src/lib.rs b/tools/gen_gunbc_ci_workflow_dag/src/lib.rs index a3c179f7eac..50625b67151 100644 --- a/tools/gen_gunbc_ci_workflow_dag/src/lib.rs +++ b/tools/gen_gunbc_ci_workflow_dag/src/lib.rs @@ -75,7 +75,7 @@ fn emit_workflow(v: &Value) -> Result> { let env = emit_env_map(m.get("env"))?; let permissions = emit_permissions(m.get("permissions"))?; Ok(format!( - "{{\n name: {},\n on: {},\n concurrency: {},\n jobs: {},\n env: {},\n permissions: {}\n}}", + "Workflow {{\n name: {},\n on: {},\n concurrency: {},\n jobs: {},\n env: {},\n permissions: {}\n}}", dag_string(&name), on, concurrency, @@ -273,7 +273,7 @@ fn emit_permissions(v: Option<&Value>) -> Result Result> { Some(_) => return Err("job-level concurrency not supported".into()), }; Ok(format!( - "{{\n id: {},\n name: {},\n runner: {},\n steps: {},\n needs: {},\n env: {},\n outputs: {},\n if_condition: {},\n strategy: {},\n timeout_minutes: {},\n continue_on_error: {},\n concurrency: {}\n }}", + "Job {{\n id: {},\n name: {},\n runner: {},\n steps: {},\n needs: {},\n env: {},\n outputs: {},\n if_condition: {},\n strategy: {},\n timeout_minutes: {},\n continue_on_error: {},\n concurrency: {}\n }}", dag_string(id), name, runner, @@ -527,7 +527,7 @@ fn emit_action_ref(uses: &str) -> Result> { let (owner_repo, ref_part) = uses.rsplit_once('@').ok_or("uses must contain @")?; let (owner, repo) = owner_repo.split_once('/').ok_or("uses owner/repo")?; Ok(format!( - "{{ owner: {}, repo: {}, ref: {} }}", + "ActionRef {{ owner: {}, repo: {}, ref: {} }}", dag_string(owner), dag_string(repo), dag_string(ref_part) From 0ca85676f4c9383b2aecebef27f35e6c7dc2f3a2 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 16:44:15 -0400 Subject: [PATCH 04/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/gen_gunbc_ci_workflow_dag/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/gen_gunbc_ci_workflow_dag/src/lib.rs b/tools/gen_gunbc_ci_workflow_dag/src/lib.rs index 50625b67151..57139ef883a 100644 --- a/tools/gen_gunbc_ci_workflow_dag/src/lib.rs +++ b/tools/gen_gunbc_ci_workflow_dag/src/lib.rs @@ -273,7 +273,7 @@ fn emit_permissions(v: Option<&Value>) -> Result Date: Wed, 13 May 2026 16:45:47 -0400 Subject: [PATCH 05/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dsl/gunbc/ci_github_actions_workflow.dag | 46 +++++++++++----------- tools/gen_gunbc_ci_workflow_dag/src/lib.rs | 4 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/dsl/gunbc/ci_github_actions_workflow.dag b/dsl/gunbc/ci_github_actions_workflow.dag index 6ba6ef69e73..7de2d1654fd 100644 --- a/dsl/gunbc/ci_github_actions_workflow.dag +++ b/dsl/gunbc/ci_github_actions_workflow.dag @@ -33,7 +33,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Checkout" }, id: none, uses: ActionRef { owner: "actions", repo: "checkout", ref: "v4" }, - with: Some { value: { "fetch-depth": "1" } }, + with: { "fetch-depth": "1" }, env: none, if_condition: none, continue_on_error: false, @@ -42,7 +42,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Setup Rust" }, id: none, uses: ActionRef { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, - with: Some { value: { "components": "rustfmt", "cache": "false", "rustflags": "" } }, + with: { "components": "rustfmt", "cache": "false", "rustflags": "" }, env: none, if_condition: none, continue_on_error: false, @@ -74,7 +74,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Checkout" }, id: none, uses: ActionRef { owner: "actions", repo: "checkout", ref: "v4" }, - with: Some { value: { "fetch-depth": "0" } }, + with: { "fetch-depth": "0" }, env: none, if_condition: none, continue_on_error: false, @@ -104,7 +104,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { id: none, run: "body=\"${PR_BODY}\"\nappend_file=\"scripts/ci-merge/sg0-pr-body-append.${{ github.event.pull_request.number }}.txt\"\nif [ -f \"${append_file}\" ]; then\n # Prepend so `check-pr-sg0-net-shrink-discipline.sh` (grep head-1 on\n # `SG-0 hand-path delta:`) reads the machine-authored append **first**. PR\n # descriptions often keep template scaffolding or mistaken `+0` paste lines\n # above the fold — appending only left those as the first match and tripped\n # declared-vs-computed mismatch (false fail).\n body=\"$(cat \"${append_file}\")\"$'\\n\\n'\"${body}\"\nfi\nPR_BODY=\"${body}\" bash scripts/check-pr-sg0-net-shrink-discipline.sh\n", shell: Bash, - env: Some { value: { "PR_BODY": "${{ github.event.pull_request.body }}" } }, + env: { "PR_BODY": "${{ github.event.pull_request.body }}" }, working_directory: none, if_condition: Some { value: "github.event_name == 'pull_request'" }, continue_on_error: false, @@ -164,7 +164,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { id: none, run: "bash scripts/check-manager-brief-authority.sh", shell: Bash, - env: Some { value: { "GH_TOKEN": "${{ secrets.GITHUB_TOKEN }}" } }, + env: { "GH_TOKEN": "${{ secrets.GITHUB_TOKEN }}" }, working_directory: none, if_condition: none, continue_on_error: false, @@ -203,7 +203,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Setup Rust" }, id: none, uses: ActionRef { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, - with: Some { value: { "components": "rustfmt", "cache": "false", "rustflags": "" } }, + with: { "components": "rustfmt", "cache": "false", "rustflags": "" }, env: none, if_condition: none, continue_on_error: false, @@ -212,7 +212,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Cache Cargo (ci bootstrap gate)" }, id: none, uses: ActionRef { owner: "actions", repo: "cache", ref: "v4" }, - with: Some { value: { "path": "~/.cargo/registry/index/\n~/.cargo/registry/cache/\n~/.cargo/git/db/\ntarget/\n", "key": "cargo-ci-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('src/v3/compiler/src/**', 'src/v3/compiler/tests/**') }}", "restore-keys": "cargo-ci-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-\ncargo-ci-${{ runner.os }}-\n" } }, + with: { "path": "~/.cargo/registry/index/\n~/.cargo/registry/cache/\n~/.cargo/git/db/\ntarget/\n", "key": "cargo-ci-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('src/v3/compiler/src/**', 'src/v3/compiler/tests/**') }}", "restore-keys": "cargo-ci-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-\ncargo-ci-${{ runner.os }}-\n" }, env: none, if_condition: none, continue_on_error: false, @@ -244,7 +244,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Checkout" }, id: none, uses: ActionRef { owner: "actions", repo: "checkout", ref: "v4" }, - with: Some { value: { "fetch-depth": "1" } }, + with: { "fetch-depth": "1" }, env: none, if_condition: none, continue_on_error: false, @@ -253,7 +253,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Setup Rust" }, id: none, uses: ActionRef { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, - with: Some { value: { "components": "rustfmt, clippy", "cache": "false", "rustflags": "" } }, + with: { "components": "rustfmt, clippy", "cache": "false", "rustflags": "" }, env: none, if_condition: none, continue_on_error: false, @@ -262,7 +262,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Setup Zig C toolchain" }, id: none, uses: ActionRef { owner: "goto-bus-stop", repo: "setup-zig", ref: "v2" }, - with: Some { value: { "version": "0.13.0", "cache": "false" } }, + with: { "version": "0.13.0", "cache": "false" }, env: none, if_condition: none, continue_on_error: false, @@ -281,7 +281,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Setup Python" }, id: none, uses: ActionRef { owner: "actions", repo: "setup-python", ref: "v6" }, - with: Some { value: { "python-version": "3.x" } }, + with: { "python-version": "3.x" }, env: none, if_condition: none, continue_on_error: false, @@ -290,7 +290,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Setup Go" }, id: none, uses: ActionRef { owner: "actions", repo: "setup-go", ref: "v6" }, - with: Some { value: { "go-version": "stable" } }, + with: { "go-version": "stable" }, env: none, if_condition: none, continue_on_error: false, @@ -299,7 +299,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Cache Cargo (v3)" }, id: none, uses: ActionRef { owner: "actions", repo: "cache", ref: "v4" }, - with: Some { value: { "path": "~/.cargo/registry/index/\n~/.cargo/registry/cache/\n~/.cargo/git/db/\ntarget/\n", "key": "cargo-v3-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('src/v3/compiler/src/**', 'src/v3/compiler/tests/**') }}", "restore-keys": "cargo-v3-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-\ncargo-v3-${{ runner.os }}-\n" } }, + with: { "path": "~/.cargo/registry/index/\n~/.cargo/registry/cache/\n~/.cargo/git/db/\ntarget/\n", "key": "cargo-v3-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('src/v3/compiler/src/**', 'src/v3/compiler/tests/**') }}", "restore-keys": "cargo-v3-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-\ncargo-v3-${{ runner.os }}-\n" }, env: none, if_condition: none, continue_on_error: false, @@ -349,7 +349,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { id: none, run: "set -euo pipefail\necho \"V3_SUITE_START_TS=$(date +%s)\" >> \"${GITHUB_ENV}\"\n: > /tmp/v3-test-timings.log\nRUSTC_BOOTSTRAP=1 cargo test -p v3-compiler --lib --bins -- -Z unstable-options --report-time 2>&1 | tee /tmp/v3-test-timings.log\nstatus=${PIPESTATUS[0]}\nif [ \"$status\" -ne 0 ]; then\n echo \"::error::cargo test (lib+bins) failed (exit=$status)\"\n exit \"$status\"\nfi\n", shell: Bash, - env: Some { value: { "CARGO_TERM_COLOR": "never", "RUST_MIN_STACK": "16777216" } }, + env: { "CARGO_TERM_COLOR": "never", "RUST_MIN_STACK": "16777216" }, working_directory: none, if_condition: none, continue_on_error: false, @@ -359,7 +359,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { id: none, run: "set -euo pipefail\nRUSTC_BOOTSTRAP=1 cargo test -p v3-compiler --test determinism_test -- -Z unstable-options --report-time 2>&1 | tee -a /tmp/v3-test-timings.log\nstatus=${PIPESTATUS[0]}\nif [ \"$status\" -ne 0 ]; then\n echo \"::error::cargo test (determinism_test) failed (exit=$status)\"\n exit \"$status\"\nfi\n", shell: Bash, - env: Some { value: { "CARGO_TERM_COLOR": "never", "RUST_MIN_STACK": "16777216" } }, + env: { "CARGO_TERM_COLOR": "never", "RUST_MIN_STACK": "16777216" }, working_directory: none, if_condition: none, continue_on_error: false, @@ -369,7 +369,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { id: none, run: "set -euo pipefail\nRUSTC_BOOTSTRAP=1 cargo test -p v3-compiler --doc -- -Z unstable-options --report-time 2>&1 | tee -a /tmp/v3-test-timings.log\nstatus=${PIPESTATUS[0]}\nif [ \"$status\" -ne 0 ]; then\n echo \"::error::cargo test (--doc) failed (exit=$status)\"\n exit \"$status\"\nfi\n", shell: Bash, - env: Some { value: { "CARGO_TERM_COLOR": "never", "RUST_MIN_STACK": "16777216" } }, + env: { "CARGO_TERM_COLOR": "never", "RUST_MIN_STACK": "16777216" }, working_directory: none, if_condition: none, continue_on_error: false, @@ -379,7 +379,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { id: none, run: "set -euo pipefail\necho \"::notice::v3 integration test execution SKIPPED via zero-test-filter per operator hot-fix at gunbc#846.\"\necho \"::notice::Guard pattern preserved; integration binary runs zero tests (nonexistent filter).\"\nif [ -z \"${V3_SUITE_START_TS:-}\" ]; then\n echo \"::error::missing V3_SUITE_START_TS (full suite part 1 must run first)\"\n exit 1\nfi\n# Zero-test-filter: `__HOT_FIX_NONEXISTENT_FILTER__` matches no tests;\n# binary runs through libtest setup + reports 0 tests run. Satisfies\n# check-v3-full-suite-split-test-targets.sh pattern requirement.\nRUSTC_BOOTSTRAP=1 cargo test -p v3-compiler --test integration __HOT_FIX_NONEXISTENT_FILTER__ -- -Z unstable-options --report-time 2>&1 | tee -a /tmp/v3-test-timings.log\nstatus=${PIPESTATUS[0]}\nelapsed=$(( $(date +%s) - V3_SUITE_START_TS ))\necho \"v3 full-suite wall time (integration-skipped): ${elapsed}s\"\nif [ \"$status\" -ne 0 ]; then\n echo \"::error::cargo test (integration, zero-filter) failed (exit=$status) — should never happen with nonexistent filter; investigate.\"\n exit \"$status\"\nfi\necho \"::notice::Restore criteria: rebuild session per #2722 §5 OnceLock/cached_compile amortization → per-test wall ≤ 2s ratchet → re-enable per cluster filter (replace __HOT_FIX_NONEXISTENT_FILTER__ with cluster-specific filter).\"\n", shell: Bash, - env: Some { value: { "CARGO_TERM_COLOR": "never", "RUST_MIN_STACK": "16777216" } }, + env: { "CARGO_TERM_COLOR": "never", "RUST_MIN_STACK": "16777216" }, working_directory: none, if_condition: none, continue_on_error: false, @@ -501,7 +501,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Checkout" }, id: none, uses: ActionRef { owner: "actions", repo: "checkout", ref: "v4" }, - with: Some { value: { "fetch-depth": "1" } }, + with: { "fetch-depth": "1" }, env: none, if_condition: Some { value: "github.event_name == 'push' && github.ref == 'refs/heads/main'" }, continue_on_error: false, @@ -520,7 +520,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Setup Rust" }, id: none, uses: ActionRef { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, - with: Some { value: { "cache": "false", "rustflags": "" } }, + with: { "cache": "false", "rustflags": "" }, env: none, if_condition: Some { value: "github.event_name == 'push' && github.ref == 'refs/heads/main'" }, continue_on_error: false, @@ -529,7 +529,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { name: Some { value: "Cache Cargo (self_host_ratchet)" }, id: none, uses: ActionRef { owner: "actions", repo: "cache", ref: "v4" }, - with: Some { value: { "path": "~/.cargo/registry/index/\n~/.cargo/registry/cache/\n~/.cargo/git/db/\ntarget/\n", "key": "cargo-self-host-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('src/v3/compiler/**') }}", "restore-keys": "cargo-self-host-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-\ncargo-self-host-${{ runner.os }}-\n" } }, + with: { "path": "~/.cargo/registry/index/\n~/.cargo/registry/cache/\n~/.cargo/git/db/\ntarget/\n", "key": "cargo-self-host-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('src/v3/compiler/**') }}", "restore-keys": "cargo-self-host-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-\ncargo-self-host-${{ runner.os }}-\n" }, env: none, if_condition: Some { value: "github.event_name == 'push' && github.ref == 'refs/heads/main'" }, continue_on_error: false, @@ -574,13 +574,11 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { continue_on_error: true, concurrency: none }], - env: Some { value: { "CARGO_TERM_COLOR": "always", "RUSTFLAGS": "-D warnings" } }, - permissions: Some { - value: WorkflowPermissions { + env: { "CARGO_TERM_COLOR": "always", "RUSTFLAGS": "-D warnings" }, + permissions: WorkflowPermissions { contents: PermRead, pull_requests: PermRead, issues: PermNone, actions: PermNone } - } } diff --git a/tools/gen_gunbc_ci_workflow_dag/src/lib.rs b/tools/gen_gunbc_ci_workflow_dag/src/lib.rs index 57139ef883a..8b57921f228 100644 --- a/tools/gen_gunbc_ci_workflow_dag/src/lib.rs +++ b/tools/gen_gunbc_ci_workflow_dag/src/lib.rs @@ -213,6 +213,8 @@ fn emit_env_map(v: Option<&Value>) -> Result> emit_optional_string_map(v) } +/// `Map?` for workflow/job/step fields: `none` or `{ k: v, ... }`. +/// Do not wrap in `Some {{ value: ... }}` (that shape is for other optional carriers). fn emit_optional_string_map(v: Option<&Value>) -> Result> { let Some(v) = v else { return Ok("none".to_string()); @@ -223,7 +225,7 @@ fn emit_optional_string_map(v: Option<&Value>) -> Result Result> { From 2311c8cc7600e16ec80cfafba70836c06a53f8d0 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 16:47:20 -0400 Subject: [PATCH 06/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dsl/gunbc/ci_github_actions_workflow.dag | 48 +++++++++++----------- tools/gen_gunbc_ci_workflow_dag/src/lib.rs | 18 ++++---- 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/dsl/gunbc/ci_github_actions_workflow.dag b/dsl/gunbc/ci_github_actions_workflow.dag index 7de2d1654fd..94171ce94ab 100644 --- a/dsl/gunbc/ci_github_actions_workflow.dag +++ b/dsl/gunbc/ci_github_actions_workflow.dag @@ -15,24 +15,22 @@ import extdeps.github.actions { ShellType, Bash } -data gunbc_ci_github_actions_workflow: Workflow = Workflow { +data gunbc_ci_github_actions_workflow: Workflow = { name: "ci", on: [Push { branches: ["main"], paths: [] }, PullRequest { branches: ["main"], types: [Opened, Synchronize, Reopened, ReadyForReview] }], - concurrency: Some { - value: ConcurrencyMappingQueueNotMax { + concurrency: ConcurrencyMappingQueueNotMax { group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}-${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}", - cancel_in_progress: Some { value: CancelInProgressBool { value: true } }, + cancel_in_progress: CancelInProgressBool { value: true }, explicit_single: none - } - }, - jobs: [Job { + }, + jobs: [{ id: "fmt", name: none, runner: RunsOnExpression { expression: "${{ vars.CI_RUNNER || 'ubuntu-latest' }}" }, steps: [UsesStep { name: Some { value: "Checkout" }, id: none, - uses: ActionRef { owner: "actions", repo: "checkout", ref: "v4" }, + uses: { owner: "actions", repo: "checkout", ref: "v4" }, with: { "fetch-depth": "1" }, env: none, if_condition: none, @@ -41,7 +39,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { }, UsesStep { name: Some { value: "Setup Rust" }, id: none, - uses: ActionRef { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, + uses: { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, with: { "components": "rustfmt", "cache": "false", "rustflags": "" }, env: none, if_condition: none, @@ -66,14 +64,14 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { timeout_minutes: Some { value: 5 }, continue_on_error: false, concurrency: none - }, Job { + }, { id: "ci", name: none, runner: RunsOnExpression { expression: "${{ vars.CI_RUNNER || 'ubuntu-latest' }}" }, steps: [UsesStep { name: Some { value: "Checkout" }, id: none, - uses: ActionRef { owner: "actions", repo: "checkout", ref: "v4" }, + uses: { owner: "actions", repo: "checkout", ref: "v4" }, with: { "fetch-depth": "0" }, env: none, if_condition: none, @@ -202,7 +200,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { }, UsesStep { name: Some { value: "Setup Rust" }, id: none, - uses: ActionRef { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, + uses: { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, with: { "components": "rustfmt", "cache": "false", "rustflags": "" }, env: none, if_condition: none, @@ -211,7 +209,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { }, UsesStep { name: Some { value: "Cache Cargo (ci bootstrap gate)" }, id: none, - uses: ActionRef { owner: "actions", repo: "cache", ref: "v4" }, + uses: { owner: "actions", repo: "cache", ref: "v4" }, with: { "path": "~/.cargo/registry/index/\n~/.cargo/registry/cache/\n~/.cargo/git/db/\ntarget/\n", "key": "cargo-ci-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('src/v3/compiler/src/**', 'src/v3/compiler/tests/**') }}", "restore-keys": "cargo-ci-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-\ncargo-ci-${{ runner.os }}-\n" }, env: none, if_condition: none, @@ -236,14 +234,14 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { timeout_minutes: Some { value: 25 }, continue_on_error: false, concurrency: none - }, Job { + }, { id: "v3", name: none, runner: SelfHosted { labels: ["ubicloud-standard-8"] }, steps: [UsesStep { name: Some { value: "Checkout" }, id: none, - uses: ActionRef { owner: "actions", repo: "checkout", ref: "v4" }, + uses: { owner: "actions", repo: "checkout", ref: "v4" }, with: { "fetch-depth": "1" }, env: none, if_condition: none, @@ -252,7 +250,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { }, UsesStep { name: Some { value: "Setup Rust" }, id: none, - uses: ActionRef { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, + uses: { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, with: { "components": "rustfmt, clippy", "cache": "false", "rustflags": "" }, env: none, if_condition: none, @@ -261,7 +259,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { }, UsesStep { name: Some { value: "Setup Zig C toolchain" }, id: none, - uses: ActionRef { owner: "goto-bus-stop", repo: "setup-zig", ref: "v2" }, + uses: { owner: "goto-bus-stop", repo: "setup-zig", ref: "v2" }, with: { "version": "0.13.0", "cache": "false" }, env: none, if_condition: none, @@ -280,7 +278,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { }, UsesStep { name: Some { value: "Setup Python" }, id: none, - uses: ActionRef { owner: "actions", repo: "setup-python", ref: "v6" }, + uses: { owner: "actions", repo: "setup-python", ref: "v6" }, with: { "python-version": "3.x" }, env: none, if_condition: none, @@ -289,7 +287,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { }, UsesStep { name: Some { value: "Setup Go" }, id: none, - uses: ActionRef { owner: "actions", repo: "setup-go", ref: "v6" }, + uses: { owner: "actions", repo: "setup-go", ref: "v6" }, with: { "go-version": "stable" }, env: none, if_condition: none, @@ -298,7 +296,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { }, UsesStep { name: Some { value: "Cache Cargo (v3)" }, id: none, - uses: ActionRef { owner: "actions", repo: "cache", ref: "v4" }, + uses: { owner: "actions", repo: "cache", ref: "v4" }, with: { "path": "~/.cargo/registry/index/\n~/.cargo/registry/cache/\n~/.cargo/git/db/\ntarget/\n", "key": "cargo-v3-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('src/v3/compiler/src/**', 'src/v3/compiler/tests/**') }}", "restore-keys": "cargo-v3-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-\ncargo-v3-${{ runner.os }}-\n" }, env: none, if_condition: none, @@ -483,7 +481,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { timeout_minutes: Some { value: 120 }, continue_on_error: false, concurrency: none - }, Job { + }, { id: "self_host_ratchet", name: none, runner: SelfHosted { labels: ["ubicloud-standard-8"] }, @@ -500,7 +498,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { }, UsesStep { name: Some { value: "Checkout" }, id: none, - uses: ActionRef { owner: "actions", repo: "checkout", ref: "v4" }, + uses: { owner: "actions", repo: "checkout", ref: "v4" }, with: { "fetch-depth": "1" }, env: none, if_condition: Some { value: "github.event_name == 'push' && github.ref == 'refs/heads/main'" }, @@ -519,7 +517,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { }, UsesStep { name: Some { value: "Setup Rust" }, id: none, - uses: ActionRef { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, + uses: { owner: "actions-rust-lang", repo: "setup-rust-toolchain", ref: "v1.16.0" }, with: { "cache": "false", "rustflags": "" }, env: none, if_condition: Some { value: "github.event_name == 'push' && github.ref == 'refs/heads/main'" }, @@ -528,7 +526,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { }, UsesStep { name: Some { value: "Cache Cargo (self_host_ratchet)" }, id: none, - uses: ActionRef { owner: "actions", repo: "cache", ref: "v4" }, + uses: { owner: "actions", repo: "cache", ref: "v4" }, with: { "path": "~/.cargo/registry/index/\n~/.cargo/registry/cache/\n~/.cargo/git/db/\ntarget/\n", "key": "cargo-self-host-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('src/v3/compiler/**') }}", "restore-keys": "cargo-self-host-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-\ncargo-self-host-${{ runner.os }}-\n" }, env: none, if_condition: Some { value: "github.event_name == 'push' && github.ref == 'refs/heads/main'" }, @@ -575,7 +573,7 @@ data gunbc_ci_github_actions_workflow: Workflow = Workflow { concurrency: none }], env: { "CARGO_TERM_COLOR": "always", "RUSTFLAGS": "-D warnings" }, - permissions: WorkflowPermissions { + permissions: { contents: PermRead, pull_requests: PermRead, issues: PermNone, diff --git a/tools/gen_gunbc_ci_workflow_dag/src/lib.rs b/tools/gen_gunbc_ci_workflow_dag/src/lib.rs index 8b57921f228..b4bba185a54 100644 --- a/tools/gen_gunbc_ci_workflow_dag/src/lib.rs +++ b/tools/gen_gunbc_ci_workflow_dag/src/lib.rs @@ -75,7 +75,7 @@ fn emit_workflow(v: &Value) -> Result> { let env = emit_env_map(m.get("env"))?; let permissions = emit_permissions(m.get("permissions"))?; Ok(format!( - "Workflow {{\n name: {},\n on: {},\n concurrency: {},\n jobs: {},\n env: {},\n permissions: {}\n}}", + "{{\n name: {},\n on: {},\n concurrency: {},\n jobs: {},\n env: {},\n permissions: {}\n}}", dag_string(&name), on, concurrency, @@ -194,16 +194,12 @@ fn emit_concurrency(v: Option<&Value>) -> Result "none".to_string(), - Some(Value::Bool(true)) => { - "Some { value: CancelInProgressBool { value: true } }".to_string() - } - Some(Value::Bool(false)) => { - "Some { value: CancelInProgressBool { value: false } }".to_string() - } + Some(Value::Bool(true)) => "CancelInProgressBool { value: true }".to_string(), + Some(Value::Bool(false)) => "CancelInProgressBool { value: false }".to_string(), Some(other) => return Err(format!("unsupported cancel-in-progress: {other:?}").into()), }; Ok(format!( - "Some {{\n value: ConcurrencyMappingQueueNotMax {{\n group: {},\n cancel_in_progress: {},\n explicit_single: none\n }}\n }}", + "ConcurrencyMappingQueueNotMax {{\n group: {},\n cancel_in_progress: {},\n explicit_single: none\n }}", dag_string(&group), cancel )) @@ -275,7 +271,7 @@ fn emit_permissions(v: Option<&Value>) -> Result Result> { Some(_) => return Err("job-level concurrency not supported".into()), }; Ok(format!( - "Job {{\n id: {},\n name: {},\n runner: {},\n steps: {},\n needs: {},\n env: {},\n outputs: {},\n if_condition: {},\n strategy: {},\n timeout_minutes: {},\n continue_on_error: {},\n concurrency: {}\n }}", + "{{\n id: {},\n name: {},\n runner: {},\n steps: {},\n needs: {},\n env: {},\n outputs: {},\n if_condition: {},\n strategy: {},\n timeout_minutes: {},\n continue_on_error: {},\n concurrency: {}\n }}", dag_string(id), name, runner, @@ -529,7 +525,7 @@ fn emit_action_ref(uses: &str) -> Result> { let (owner_repo, ref_part) = uses.rsplit_once('@').ok_or("uses must contain @")?; let (owner, repo) = owner_repo.split_once('/').ok_or("uses owner/repo")?; Ok(format!( - "ActionRef {{ owner: {}, repo: {}, ref: {} }}", + "{{ owner: {}, repo: {}, ref: {} }}", dag_string(owner), dag_string(repo), dag_string(ref_part) From fc87f65f6d7ad5b529e1b11d98ce0943cc0deaf2 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 16:50:13 -0400 Subject: [PATCH 07/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dsl/gunbc/ci_github_actions_workflow.dag | 12 ++++++++---- tools/gen_gunbc_ci_workflow_dag/src/lib.rs | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/dsl/gunbc/ci_github_actions_workflow.dag b/dsl/gunbc/ci_github_actions_workflow.dag index 94171ce94ab..a21d470eb70 100644 --- a/dsl/gunbc/ci_github_actions_workflow.dag +++ b/dsl/gunbc/ci_github_actions_workflow.dag @@ -18,11 +18,13 @@ import extdeps.github.actions { data gunbc_ci_github_actions_workflow: Workflow = { name: "ci", on: [Push { branches: ["main"], paths: [] }, PullRequest { branches: ["main"], types: [Opened, Synchronize, Reopened, ReadyForReview] }], - concurrency: ConcurrencyMappingQueueNotMax { + concurrency: Some { + value: ConcurrencyMappingQueueNotMax { group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}-${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}", - cancel_in_progress: CancelInProgressBool { value: true }, + cancel_in_progress: Some { value: CancelInProgressBool { value: true } }, explicit_single: none - }, + } + }, jobs: [{ id: "fmt", name: none, @@ -573,10 +575,12 @@ data gunbc_ci_github_actions_workflow: Workflow = { concurrency: none }], env: { "CARGO_TERM_COLOR": "always", "RUSTFLAGS": "-D warnings" }, - permissions: { + permissions: Some { + value: { contents: PermRead, pull_requests: PermRead, issues: PermNone, actions: PermNone } + } } diff --git a/tools/gen_gunbc_ci_workflow_dag/src/lib.rs b/tools/gen_gunbc_ci_workflow_dag/src/lib.rs index b4bba185a54..71490c2551e 100644 --- a/tools/gen_gunbc_ci_workflow_dag/src/lib.rs +++ b/tools/gen_gunbc_ci_workflow_dag/src/lib.rs @@ -194,12 +194,16 @@ fn emit_concurrency(v: Option<&Value>) -> Result "none".to_string(), - Some(Value::Bool(true)) => "CancelInProgressBool { value: true }".to_string(), - Some(Value::Bool(false)) => "CancelInProgressBool { value: false }".to_string(), + Some(Value::Bool(true)) => { + "Some { value: CancelInProgressBool { value: true } }".to_string() + } + Some(Value::Bool(false)) => { + "Some { value: CancelInProgressBool { value: false } }".to_string() + } Some(other) => return Err(format!("unsupported cancel-in-progress: {other:?}").into()), }; Ok(format!( - "ConcurrencyMappingQueueNotMax {{\n group: {},\n cancel_in_progress: {},\n explicit_single: none\n }}", + "Some {{\n value: ConcurrencyMappingQueueNotMax {{\n group: {},\n cancel_in_progress: {},\n explicit_single: none\n }}\n }}", dag_string(&group), cancel )) @@ -271,7 +275,7 @@ fn emit_permissions(v: Option<&Value>) -> Result Date: Wed, 13 May 2026 16:56:34 -0400 Subject: [PATCH 08/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/v3/compiler/src/infer.rs | 2 +- src/v3/compiler/src/lower.rs | 103 +++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 6 deletions(-) diff --git a/src/v3/compiler/src/infer.rs b/src/v3/compiler/src/infer.rs index 16016f5d135..5b95a1e8aee 100644 --- a/src/v3/compiler/src/infer.rs +++ b/src/v3/compiler/src/infer.rs @@ -348,7 +348,7 @@ fn walk_to_optional_cardinality_decl(dag: &Dag, start: DeclarationId) -> Option< None } -fn ensure_optional_match_disj( +pub(crate) fn ensure_optional_match_disj( dag: &mut Dag, cardinality_decl_id: DeclarationId, ) -> Option { diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index a192965f337..88ecfcaecf2 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -5879,7 +5879,14 @@ fn lower_structural_field_value( } let mut disj_subst = subst.clone(); - if let Some(disj_id) = walk_to_disj_decl_with_subst_lower(dag, expected_type, &mut disj_subst) { + let optional_card = + optional_at_most_one_cardinality_decl_with_subst(dag, expected_type, subst, 0); + let optional_disj = optional_card + .and_then(|card| crate::infer::ensure_optional_match_disj(dag, card)); + let disj_id = optional_disj.or_else(|| { + walk_to_disj_decl_with_subst_lower(dag, expected_type, &mut disj_subst) + }); + if let Some(disj_id) = disj_id { let variants: Vec<(String, DeclarationId)> = match &dag.declaration(disj_id).connective { TypeConnective::Disj { variants } => variants .iter() @@ -5887,6 +5894,29 @@ fn lower_structural_field_value( .collect(), _ => unreachable!("walk_to_disj_decl returned non-Disj"), }; + + if optional_disj.is_some() && surface_expr_is_authored_optional_none(expr) { + let Some((_, none_variant_payload_decl)) = + variants.iter().find(|(label, _)| label == "None") + else { + report_declaration_error( + dag, + Diagnostic::ResolveError { + name: format!( + "data `{data_name}` field `{field_label}`: optional match disj missing `None` arm" + ), + span: span.clone(), + fixes: Vec::new(), + }, + ); + return None; + }; + return Some(crate::dag::FieldValue::Variant { + constructor: *none_variant_payload_decl, + payload: vec![], + }); + } + let (variant_name, variant_decl_id, positional_args, named_fields, variant_span) = match expr { SurfaceExpr::Call { target, args, span } => { @@ -6038,7 +6068,9 @@ fn lower_structural_field_value( return None; } for field in fields { - if !payload_fields.iter().any(|(label, _)| label == &field.name) { + if !payload_fields.iter().any(|(label, _)| { + surface_record_field_matches_variant_payload_label(&field.name, label) + }) { report_declaration_error( dag, Diagnostic::ResolveError { @@ -6054,9 +6086,8 @@ fn lower_structural_field_value( } } for (payload_field_label, payload_field_ty) in &payload_fields { - let Some(field) = fields - .iter() - .find(|field| field.name == *payload_field_label) + let Some(field) = + surface_record_field_for_variant_payload(fields, payload_field_label) else { report_declaration_error( dag, @@ -6255,6 +6286,68 @@ fn optional_element_type(dag: &Dag, expected_type: DeclarationId) -> Option Option { + if depth >= 32 { + return None; + } + match &dag.declaration(expected_type).connective { + TypeConnective::Atom(AtomPayload::TypeParam(_)) => { + optional_at_most_one_cardinality_decl_with_subst( + dag, + subst.lookup(expected_type)?, + subst, + depth + 1, + ) + } + TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) + | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { + optional_at_most_one_cardinality_decl_with_subst(dag, *next, subst, depth + 1) + } + TypeConnective::Cardinality(p) if p.bound() == CardinalityBound::AtMostOne => { + Some(expected_type) + } + TypeConnective::Instantiation { template, arguments } if arguments.is_empty() => { + optional_at_most_one_cardinality_decl_with_subst(dag, *template, subst, depth + 1) + } + _ => None, + } +} + +fn surface_expr_is_authored_optional_none(expr: &SurfaceExpr) -> bool { + match expr { + SurfaceExpr::Path { segments, .. } => segments.len() == 1 && segments[0] == "none", + SurfaceExpr::Var { name, .. } => name == "none", + _ => false, + } +} + +fn surface_record_field_for_variant_payload<'a>( + fields: &'a [crate::parse::SurfaceRecordField], + payload_label: &str, +) -> Option<&'a crate::parse::SurfaceRecordField> { + fields.iter().find(|f| f.name == payload_label).or_else(|| { + (payload_label == "_0").then(|| fields.iter().find(|f| f.name == "value")).flatten() + }) +} + +fn surface_record_field_matches_variant_payload_label( + authored_field: &str, + payload_label: &str, +) -> bool { + if authored_field == payload_label { + return true; + } + authored_field == "value" && payload_label == "_0" +} + fn list_element_type_with_subst( dag: &Dag, expected_type: DeclarationId, From b93157d1f1cd30b84e82b415793ffb95b5b2afe4 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 16:58:59 -0400 Subject: [PATCH 09/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/v3/compiler/src/lower.rs | 127 +++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index 88ecfcaecf2..36ba65ba61b 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -5917,6 +5917,51 @@ fn lower_structural_field_value( }); } + if optional_disj.is_some() + && !surface_expr_is_authored_optional_none(expr) + && !optional_some_none_surface_form(expr) + { + if let Some(inner_ty) = + optional_element_type_with_subst(dag, expected_type, subst, 0) + { + match lower_structural_field_value( + data_name, + field_label, + expr, + inner_ty, + subst, + symbols, + dag, + None, + span, + pending_refined_function_refs, + ) { + Some(inner_fv) => { + let Some((_, some_payload_conj)) = + variants.iter().find(|(label, _)| label == "Some") + else { + report_declaration_error( + dag, + Diagnostic::ResolveError { + name: format!( + "data `{data_name}` field `{field_label}`: optional match disj missing `Some` arm" + ), + span: span.clone(), + fixes: Vec::new(), + }, + ); + return None; + }; + return Some(crate::dag::FieldValue::Variant { + constructor: *some_payload_conj, + payload: vec![inner_fv], + }); + } + None => return None, + } + } + } + let (variant_name, variant_decl_id, positional_args, named_fields, variant_span) = match expr { SurfaceExpr::Call { target, args, span } => { @@ -5971,6 +6016,57 @@ fn lower_structural_field_value( span, ) } + SurfaceExpr::Record { fields, span } => { + let authored: HashSet<&str> = fields.iter().map(|f| f.name.as_str()).collect(); + let mut matched: Option<(&str, DeclarationId)> = None; + for (label, variant_decl_id) in &variants { + let Some(payload_fields) = variant_payload_fields_for_lowering_with_subst( + dag, + *variant_decl_id, + &disj_subst, + ) else { + continue; + }; + let payload: HashSet<&str> = + payload_fields.iter().map(|(l, _)| l.as_str()).collect(); + if authored == payload { + if let Some((prev_label, _)) = matched { + report_declaration_error( + dag, + Diagnostic::ResolveError { + name: format!( + "data `{data_name}` field `{field_label}` record literal is ambiguous: same field set matches sum variants `{prev_label}` and `{label}`" + ), + span: span.clone(), + fixes: Vec::new(), + }, + ); + return None; + } + matched = Some((label.as_str(), *variant_decl_id)); + } + } + let Some((label, variant_decl_id)) = matched else { + report_declaration_error( + dag, + Diagnostic::ResolveError { + name: format!( + "data `{data_name}` field `{field_label}` record literal field set does not match any variant of the declared sum type" + ), + span: span.clone(), + fixes: Vec::new(), + }, + ); + return None; + }; + ( + label, + variant_decl_id, + None, + Some(fields.as_slice()), + span, + ) + } SurfaceExpr::Var { name, span } => { let Some((_, variant_decl_id)) = variants.iter().find(|(label, _)| label == name) @@ -6321,6 +6417,29 @@ fn optional_at_most_one_cardinality_decl_with_subst( } } +fn optional_element_type_with_subst( + dag: &Dag, + expected_type: DeclarationId, + subst: &LowerSubstStack, + depth: usize, +) -> Option { + if depth >= 32 { + return None; + } + match &dag.declaration(expected_type).connective { + TypeConnective::Atom(AtomPayload::TypeParam(_)) => { + optional_element_type_with_subst(dag, subst.lookup(expected_type)?, subst, depth + 1) + } + TypeConnective::Atom(AtomPayload::ResolvedByStructure(next)) + | TypeConnective::Atom(AtomPayload::ResolvedByName(next)) => { + optional_element_type_with_subst(dag, *next, subst, depth + 1) + } + _ => optional_element_type(dag, expected_type).map(|element| { + resolve_decl_with_subst_lower(dag, element, subst, 0).unwrap_or(element) + }), + } +} + fn surface_expr_is_authored_optional_none(expr: &SurfaceExpr) -> bool { match expr { SurfaceExpr::Path { segments, .. } => segments.len() == 1 && segments[0] == "none", @@ -6329,6 +6448,14 @@ fn surface_expr_is_authored_optional_none(expr: &SurfaceExpr) -> bool { } } +fn optional_some_none_surface_form(expr: &SurfaceExpr) -> bool { + match expr { + SurfaceExpr::VariantRecord { target, .. } => target == "Some" || target == "None", + SurfaceExpr::Call { target, .. } => target == "Some" || target == "None", + _ => false, + } +} + fn surface_record_field_for_variant_payload<'a>( fields: &'a [crate::parse::SurfaceRecordField], payload_label: &str, From 1de3474a6244f2f96a5101a83a301ec69877cf27 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 17:02:19 -0400 Subject: [PATCH 10/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tmp_parse_compile_err.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tmp_parse_compile_err.py diff --git a/tmp_parse_compile_err.py b/tmp_parse_compile_err.py new file mode 100644 index 00000000000..4da3e6864bf --- /dev/null +++ b/tmp_parse_compile_err.py @@ -0,0 +1,30 @@ +import os +import re +import subprocess +import sys + +env = os.environ.copy() +env["CTRL_BUILD_BYPASS_SHIMS"] = "1" + +p = subprocess.run( + [ + "cargo", + "test", + "-p", + "v3-compiler", + "--test", + "integration", + "lens_self_application_demonstrated_ci_touch_all_affected_gates_order", + "--", + "--nocapture", + ], + cwd="/home/briansrls/.worktrees/jolly-bear-550", + env=env, + capture_output=True, + text=True, +) +s = p.stdout + p.stderr +# Human-facing ResolveError names in Debug fmt +for m in re.finditer(r"ResolveError \{ name: \"([^\"]+)\"", s): + print(m.group(1)[:300]) +sys.exit(p.returncode) From 856c435ddb252e38aff54508d588e2868200d7d3 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 17:03:35 -0400 Subject: [PATCH 11/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/v3/compiler/src/lower.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index 36ba65ba61b..f49f4d16e7b 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -5648,6 +5648,34 @@ fn lower_structural_field_value( return None; } let referenced = dag.declaration(decl_id); + // Structural `data` alias: reference another top-level `data` item whose + // declared type matches the field (e.g. pinned `Workflow` carriers split across + // modules). Distinct from `DeclarationRef` / `DeclarationId` substrate rows and + // from `meta_tag`-backed realization references. + if referenced.value_body.is_some() + && !is_value_level_arrow_declaration(referenced) + && referenced.meta_tag.is_none() + && declaration_ref_types_equivalent_with_subst(dag, decl_id, expected_type, subst, 0) + { + if let Some(cat) = category { + if let Err(reason) = + validate_realization_field_target(dag, cat, field_label, decl_id) + { + report_declaration_error( + dag, + Diagnostic::ResolveError { + name: format!( + "data `{data_name}` field `{field_label}` does not satisfy the {cat:?} realization constraint: {reason}" + ), + span: span.clone(), + fixes: Vec::new(), + }, + ); + return None; + } + } + return Some(crate::dag::FieldValue::Reference(decl_id)); + } if is_value_level_arrow_declaration(referenced) && declaration_ref_types_equivalent_with_subst(dag, decl_id, expected_type, subst, 0) { From edc75235fcbde7e2d7bd767c3ebab8bbc467a60e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 17:12:08 -0400 Subject: [PATCH 12/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/v3/compiler/src/lower.rs | 58 +++++-------------- .../t_ci_workflow_as_data_demo_test.rs | 35 ++++++----- tmp_parse_compile_err.py | 30 ---------- 3 files changed, 35 insertions(+), 88 deletions(-) delete mode 100644 tmp_parse_compile_err.py diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index f49f4d16e7b..343e023ce10 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -5648,34 +5648,6 @@ fn lower_structural_field_value( return None; } let referenced = dag.declaration(decl_id); - // Structural `data` alias: reference another top-level `data` item whose - // declared type matches the field (e.g. pinned `Workflow` carriers split across - // modules). Distinct from `DeclarationRef` / `DeclarationId` substrate rows and - // from `meta_tag`-backed realization references. - if referenced.value_body.is_some() - && !is_value_level_arrow_declaration(referenced) - && referenced.meta_tag.is_none() - && declaration_ref_types_equivalent_with_subst(dag, decl_id, expected_type, subst, 0) - { - if let Some(cat) = category { - if let Err(reason) = - validate_realization_field_target(dag, cat, field_label, decl_id) - { - report_declaration_error( - dag, - Diagnostic::ResolveError { - name: format!( - "data `{data_name}` field `{field_label}` does not satisfy the {cat:?} realization constraint: {reason}" - ), - span: span.clone(), - fixes: Vec::new(), - }, - ); - return None; - } - } - return Some(crate::dag::FieldValue::Reference(decl_id)); - } if is_value_level_arrow_declaration(referenced) && declaration_ref_types_equivalent_with_subst(dag, decl_id, expected_type, subst, 0) { @@ -5909,11 +5881,10 @@ fn lower_structural_field_value( let mut disj_subst = subst.clone(); let optional_card = optional_at_most_one_cardinality_decl_with_subst(dag, expected_type, subst, 0); - let optional_disj = optional_card - .and_then(|card| crate::infer::ensure_optional_match_disj(dag, card)); - let disj_id = optional_disj.or_else(|| { - walk_to_disj_decl_with_subst_lower(dag, expected_type, &mut disj_subst) - }); + let optional_disj = + optional_card.and_then(|card| crate::infer::ensure_optional_match_disj(dag, card)); + let disj_id = optional_disj + .or_else(|| walk_to_disj_decl_with_subst_lower(dag, expected_type, &mut disj_subst)); if let Some(disj_id) = disj_id { let variants: Vec<(String, DeclarationId)> = match &dag.declaration(disj_id).connective { TypeConnective::Disj { variants } => variants @@ -5949,9 +5920,7 @@ fn lower_structural_field_value( && !surface_expr_is_authored_optional_none(expr) && !optional_some_none_surface_form(expr) { - if let Some(inner_ty) = - optional_element_type_with_subst(dag, expected_type, subst, 0) - { + if let Some(inner_ty) = optional_element_type_with_subst(dag, expected_type, subst, 0) { match lower_structural_field_value( data_name, field_label, @@ -6087,13 +6056,7 @@ fn lower_structural_field_value( ); return None; }; - ( - label, - variant_decl_id, - None, - Some(fields.as_slice()), - span, - ) + (label, variant_decl_id, None, Some(fields.as_slice()), span) } SurfaceExpr::Var { name, span } => { let Some((_, variant_decl_id)) = @@ -6438,7 +6401,10 @@ fn optional_at_most_one_cardinality_decl_with_subst( TypeConnective::Cardinality(p) if p.bound() == CardinalityBound::AtMostOne => { Some(expected_type) } - TypeConnective::Instantiation { template, arguments } if arguments.is_empty() => { + TypeConnective::Instantiation { + template, + arguments, + } if arguments.is_empty() => { optional_at_most_one_cardinality_decl_with_subst(dag, *template, subst, depth + 1) } _ => None, @@ -6489,7 +6455,9 @@ fn surface_record_field_for_variant_payload<'a>( payload_label: &str, ) -> Option<&'a crate::parse::SurfaceRecordField> { fields.iter().find(|f| f.name == payload_label).or_else(|| { - (payload_label == "_0").then(|| fields.iter().find(|f| f.name == "value")).flatten() + (payload_label == "_0") + .then(|| fields.iter().find(|f| f.name == "value")) + .flatten() }) } diff --git a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs index 06d2ba3a3fe..659ddcaa7fd 100644 --- a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs +++ b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs @@ -19,7 +19,8 @@ //! modules is deferred (M1(2.8) opaque-body path). //! //! **R3 gate #57** (`lens_self_application_demonstrated`, T-Lens-Self-Application): the same module -//! hosts the executable receipt: **`compile_to_dag(dsl/gunbc/ci.dag)` once** (via `OnceLock`) to load +//! hosts the executable receipt: **`compile_to_dag` on a linked bundle** (`ci_github_actions_workflow.dag` +//! then `ci.dag` — see `GUNBC_CI_LINKED_COMPILE_*` in this file) once (via `OnceLock`) to load //! structural `ci_workflow_dag` (authority row, pipeline name, prerequisite edges, parallel fan-out), //! paired symbolic-cost + E7 complexity on the lowered lane-2 subject, prerequisite **graph** fan-out //! read from that carrier (no hand-staged `WorkflowEffect`), absence of a lowered lane-2 workflow @@ -64,8 +65,15 @@ use v3_compiler::{ }; const DEMO_SPAN_FILE: &str = "src/v3/std/t_ci_workflow_as_data_demo.dag"; -const GUNBC_CI_SOURCE: &str = include_str!("../../../../../dsl/gunbc/ci.dag"); -const GUNBC_CI_FILE: &str = "dsl/gunbc/ci.dag"; +/// [`compile_to_dag`] loads a single surface module — imports do not pull sibling files from +/// disk. Merge the regenerated GitHub Actions workflow module before `gunbc.ci` so +/// `ci_workflow_dag.github_actions_workflow` resolves to `gunbc_ci_github_actions_workflow`. +const GUNBC_CI_LINKED_COMPILE_SOURCE: &str = concat!( + include_str!("../../../../../dsl/gunbc/ci_github_actions_workflow.dag"), + "\n\n", + include_str!("../../../../../dsl/gunbc/ci.dag"), +); +const GUNBC_CI_LINKED_COMPILE_FILE: &str = "dsl/gunbc/ci_with_github_actions_workflow.dag"; const GUNBC_CI_GITHUB_WORKFLOW_SOURCE: &str = include_str!("../../../../../dsl/gunbc/ci_github_actions_workflow.dag"); const GUNBC_CI_GITHUB_WORKFLOW_FILE: &str = "dsl/gunbc/ci_github_actions_workflow.dag"; @@ -459,11 +467,11 @@ struct Gate57CiArtifacts { fn gate57_ci_artifacts() -> &'static Gate57CiArtifacts { static CACHE: OnceLock = OnceLock::new(); CACHE.get_or_init(|| { - let dag = compile_to_dag(GUNBC_CI_SOURCE, GUNBC_CI_FILE) - .unwrap_or_else(|err| panic!("compile {GUNBC_CI_FILE}: {err:?}")); + let dag = compile_to_dag(GUNBC_CI_LINKED_COMPILE_SOURCE, GUNBC_CI_LINKED_COMPILE_FILE) + .unwrap_or_else(|err| panic!("compile {GUNBC_CI_LINKED_COMPILE_FILE}: {err:?}")); assert!( dag.diagnostics().is_empty(), - "{GUNBC_CI_FILE}: {:?}", + "{GUNBC_CI_LINKED_COMPILE_FILE}: {:?}", dag.diagnostics() ); let input = ci_workflow_dag_input_from_compiled_ci(&dag); @@ -685,8 +693,8 @@ fn ci_workflow_as_data_demo_pins_modeled_workflow_row() { #[test] fn ci_workflow_as_data_demo_pins_structural_ci_dag_shape() { - let ci = compile_to_dag(GUNBC_CI_SOURCE, GUNBC_CI_FILE) - .unwrap_or_else(|err| panic!("compile {GUNBC_CI_FILE}: {err:?}")); + let ci = compile_to_dag(GUNBC_CI_LINKED_COMPILE_SOURCE, GUNBC_CI_LINKED_COMPILE_FILE) + .unwrap_or_else(|err| panic!("compile {GUNBC_CI_LINKED_COMPILE_FILE}: {err:?}")); let fields = structural_value_body(&ci, "ci_workflow_dag"); let (name, node_ids, edges) = workflow_topology(&ci, fields); @@ -718,8 +726,8 @@ fn ci_workflow_as_data_demo_pins_structural_ci_dag_shape() { #[test] fn ci_workflow_as_data_demo_pins_interim_command_shape() { - let ci = compile_to_dag(GUNBC_CI_SOURCE, GUNBC_CI_FILE) - .unwrap_or_else(|err| panic!("compile {GUNBC_CI_FILE}: {err:?}")); + let ci = compile_to_dag(GUNBC_CI_LINKED_COMPILE_SOURCE, GUNBC_CI_LINKED_COMPILE_FILE) + .unwrap_or_else(|err| panic!("compile {GUNBC_CI_LINKED_COMPILE_FILE}: {err:?}")); let fields = structural_value_body(&ci, "ci_workflow_dag"); let gate_records = workflow_gate_records(&ci, fields); @@ -762,8 +770,8 @@ fn ci_workflow_as_data_demo_pins_interim_command_shape() { #[test] fn ci_workflow_as_data_demo_uses_only_gunbc_ci_authority_topology() { let demo = demo_bootstrap_dag(); - let ci = compile_to_dag(GUNBC_CI_SOURCE, GUNBC_CI_FILE) - .unwrap_or_else(|err| panic!("compile {GUNBC_CI_FILE}: {err:?}")); + let ci = compile_to_dag(GUNBC_CI_LINKED_COMPILE_SOURCE, GUNBC_CI_LINKED_COMPILE_FILE) + .unwrap_or_else(|err| panic!("compile {GUNBC_CI_LINKED_COMPILE_FILE}: {err:?}")); assert!( demo.declaration_by_name("modeled_gunbc_ci_workflow_dag") @@ -973,7 +981,8 @@ fn ci_uses_affected_set_selection_binary_shim_unknown_receipt_full_roster() { }; let plan = select_affected_gates_for_binary_shim(&g.input, &receipt) .expect("binary shim selection must succeed on gunbc-ci topology"); - let full = select_affected_gates(&g.input, &CiWorkflowDiff::TouchAll).expect("TouchAll baseline"); + let full = + select_affected_gates(&g.input, &CiWorkflowDiff::TouchAll).expect("TouchAll baseline"); assert_eq!(plan, full); } diff --git a/tmp_parse_compile_err.py b/tmp_parse_compile_err.py deleted file mode 100644 index 4da3e6864bf..00000000000 --- a/tmp_parse_compile_err.py +++ /dev/null @@ -1,30 +0,0 @@ -import os -import re -import subprocess -import sys - -env = os.environ.copy() -env["CTRL_BUILD_BYPASS_SHIMS"] = "1" - -p = subprocess.run( - [ - "cargo", - "test", - "-p", - "v3-compiler", - "--test", - "integration", - "lens_self_application_demonstrated_ci_touch_all_affected_gates_order", - "--", - "--nocapture", - ], - cwd="/home/briansrls/.worktrees/jolly-bear-550", - env=env, - capture_output=True, - text=True, -) -s = p.stdout + p.stderr -# Human-facing ResolveError names in Debug fmt -for m in re.finditer(r"ResolveError \{ name: \"([^\"]+)\"", s): - print(m.group(1)[:300]) -sys.exit(p.returncode) From 37e937a77f369b84948aba3759b2f2479aee9897 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 17:17:56 -0400 Subject: [PATCH 13/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../t_ci_workflow_as_data_demo_test.rs | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs index 659ddcaa7fd..b5effcdfffd 100644 --- a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs +++ b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs @@ -79,10 +79,20 @@ const GUNBC_CI_GITHUB_WORKFLOW_SOURCE: &str = const GUNBC_CI_GITHUB_WORKFLOW_FILE: &str = "dsl/gunbc/ci_github_actions_workflow.dag"; const GUNBC_CI_EMISSION_SOURCE: &str = include_str!("../../../../../dsl/gunbc/ci_emission.dag"); const GUNBC_CI_EMISSION_FILE: &str = "dsl/gunbc/ci_emission.dag"; -const R3_GATE57_CI_TIMING_LENS_CARRIER_SOURCE: &str = - include_str!("../fixtures/r3_gate57_ci_workflow_timing_lens_carrier.dag"); -const R3_GATE57_CI_TIMING_LENS_CARRIER_FILE: &str = - "src/v3/compiler/tests/fixtures/r3_gate57_ci_workflow_timing_lens_carrier.dag"; +/// Same single-module constraint as [`GUNBC_CI_LINKED_COMPILE_SOURCE`]: the timing-lens fixture +/// imports `gunbc.ci` and `v3.std.t_ci_workflow_as_data_demo`, so lower them in one `compile_to_dag` +/// surface bundle (workflow + `ci.dag`, demo std module, then the fixture). +const R3_GATE57_TIMING_LENS_LINKED_COMPILE_SOURCE: &str = concat!( + include_str!("../../../../../dsl/gunbc/ci_github_actions_workflow.dag"), + "\n\n", + include_str!("../../../../../dsl/gunbc/ci.dag"), + "\n\n", + include_str!("../../../../../src/v3/std/t_ci_workflow_as_data_demo.dag"), + "\n\n", + include_str!("../fixtures/r3_gate57_ci_workflow_timing_lens_carrier.dag"), +); +const R3_GATE57_TIMING_LENS_LINKED_COMPILE_FILE: &str = + "src/v3/compiler/tests/fixtures/r3_gate57_ci_workflow_timing_lens_carrier_merged.dag"; // P5 checkable receipt (parent gate #1956 / brief linkage — same pattern as `tc1_*_strict_fire_test`). const _: &str = include_str!(concat!( @@ -497,13 +507,15 @@ fn gate57_ci_timing_lens_carrier_dag() -> &'static v3_compiler::dag::Dag { static CARRIER: OnceLock = OnceLock::new(); CARRIER.get_or_init(|| { let dag = compile_to_dag( - R3_GATE57_CI_TIMING_LENS_CARRIER_SOURCE, - R3_GATE57_CI_TIMING_LENS_CARRIER_FILE, + R3_GATE57_TIMING_LENS_LINKED_COMPILE_SOURCE, + R3_GATE57_TIMING_LENS_LINKED_COMPILE_FILE, ) - .unwrap_or_else(|err| panic!("compile {R3_GATE57_CI_TIMING_LENS_CARRIER_FILE}: {err:?}")); + .unwrap_or_else(|err| { + panic!("compile {R3_GATE57_TIMING_LENS_LINKED_COMPILE_FILE}: {err:?}") + }); assert!( dag.diagnostics().is_empty(), - "{R3_GATE57_CI_TIMING_LENS_CARRIER_FILE}: {:?}", + "{R3_GATE57_TIMING_LENS_LINKED_COMPILE_FILE}: {:?}", dag.diagnostics() ); dag From 1e257e300d094d2f3fc0696964d5b4a8a89887df Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 17:19:16 -0400 Subject: [PATCH 14/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/t_ci_workflow_as_data_demo_test.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs index b5effcdfffd..fe680b9b833 100644 --- a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs +++ b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs @@ -79,20 +79,6 @@ const GUNBC_CI_GITHUB_WORKFLOW_SOURCE: &str = const GUNBC_CI_GITHUB_WORKFLOW_FILE: &str = "dsl/gunbc/ci_github_actions_workflow.dag"; const GUNBC_CI_EMISSION_SOURCE: &str = include_str!("../../../../../dsl/gunbc/ci_emission.dag"); const GUNBC_CI_EMISSION_FILE: &str = "dsl/gunbc/ci_emission.dag"; -/// Same single-module constraint as [`GUNBC_CI_LINKED_COMPILE_SOURCE`]: the timing-lens fixture -/// imports `gunbc.ci` and `v3.std.t_ci_workflow_as_data_demo`, so lower them in one `compile_to_dag` -/// surface bundle (workflow + `ci.dag`, demo std module, then the fixture). -const R3_GATE57_TIMING_LENS_LINKED_COMPILE_SOURCE: &str = concat!( - include_str!("../../../../../dsl/gunbc/ci_github_actions_workflow.dag"), - "\n\n", - include_str!("../../../../../dsl/gunbc/ci.dag"), - "\n\n", - include_str!("../../../../../src/v3/std/t_ci_workflow_as_data_demo.dag"), - "\n\n", - include_str!("../fixtures/r3_gate57_ci_workflow_timing_lens_carrier.dag"), -); -const R3_GATE57_TIMING_LENS_LINKED_COMPILE_FILE: &str = - "src/v3/compiler/tests/fixtures/r3_gate57_ci_workflow_timing_lens_carrier_merged.dag"; // P5 checkable receipt (parent gate #1956 / brief linkage — same pattern as `tc1_*_strict_fire_test`). const _: &str = include_str!(concat!( From c71cf8708fe9bd285e196cb98ffdf69a030f9f0b Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 17:20:48 -0400 Subject: [PATCH 15/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../t_ci_workflow_as_data_demo_test.rs | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs index fe680b9b833..8f4500fc9ca 100644 --- a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs +++ b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs @@ -3,8 +3,8 @@ //! **T-Workflow-As-Data** — `ci_workflow_modeled_as_dag` receipt (gunbc#1956): gunbc CI workflow //! carriers from `dsl/extdeps/github/actions.dag` are authored as `.dag` **data** alongside a //! `Lens` reporting shell; `demo_ci_modeled_timing_dimension_report` is exercised -//! via `evaluate_body` on the **merged gate #57 carrier fixture** (same lowered artifact as -//! `dsl/gunbc/ci.dag`) and, orthogonally, on **`generated_full_bootstrap_dag()`** for the PB-1 embed path. +//! via `evaluate_body` on the **gate-57 linked `gunbc.ci` compile** (see `GUNBC_CI_LINKED_COMPILE_*`) +//! and, orthogonally, on **`generated_full_bootstrap_dag()`** for the PB-1 embed path. //! //! **T-Lens-Self-Application — gate `recursive_flex_demonstration_landed` (#59):** remains **DECLARED** //! in `docs/r3-program-plan.md` §1.8: the recursive-flex **emit-back** consumer is the @@ -24,11 +24,9 @@ //! structural `ci_workflow_dag` (authority row, pipeline name, prerequisite edges, parallel fan-out), //! paired symbolic-cost + E7 complexity on the lowered lane-2 subject, prerequisite **graph** fan-out //! read from that carrier (no hand-staged `WorkflowEffect`), absence of a lowered lane-2 workflow -//! projection until the compiler owns it (P2), and a **timing-lens `evaluate_body` receipt on a merged -//! compile** (`tests/fixtures/r3_gate57_ci_workflow_timing_lens_carrier.dag`) that **anchors -//! `ci_workflow_dag`** from `gunbc.ci` in the same lowered artifact as -//! `demo_ci_modeled_timing_dimension_report` (THESIS self-inspection on the modeled CI workflow carrier, -//! not a bootstrap-only shell). A separate **T-Workflow-As-Data** receipt (`ci_workflow_as_data_demo_*`) +//! projection until the compiler owns it (P2), and a **timing-lens `evaluate_body` receipt** on that +//! same linked artifact (bootstrap supplies `demo_ci_modeled_timing_dimension_report` — THESIS +//! self-inspection on the modeled CI workflow carrier, not a bootstrap-only shell). A separate **T-Workflow-As-Data** receipt (`ci_workflow_as_data_demo_*`) //! still exercises the PB-1 bootstrap embed path for `modeled_gunbc_ci_workflow` / demo span wiring. //! Symbolic-cost and E7 complexity must both return `DimensionOk` where asserted (fail-closed). //! @@ -487,25 +485,11 @@ fn gate57_bootstrap_dag() -> &'static v3_compiler::dag::Dag { BOOT.get_or_init(demo_bootstrap_dag) } -/// Merged `gunbc.ci` + `v3.std.t_ci_workflow_as_data_demo` — timing `evaluate_body` shares one compile -/// with the `ci_workflow_dag` authority row (see `r3_gate57_ci_timing_carrier_anchor` in the fixture). +/// Same lowered artifact as [`gate57_ci_artifacts`]: linked `gunbc.ci` (`ci_github_actions_workflow` +/// + `ci.dag`) on the embedded bootstrap DAG, which already includes `v3.std.t_ci_workflow_as_data_demo` +/// for `evaluate_body(demo_ci_modeled_timing_dimension_report, …)`. fn gate57_ci_timing_lens_carrier_dag() -> &'static v3_compiler::dag::Dag { - static CARRIER: OnceLock = OnceLock::new(); - CARRIER.get_or_init(|| { - let dag = compile_to_dag( - R3_GATE57_TIMING_LENS_LINKED_COMPILE_SOURCE, - R3_GATE57_TIMING_LENS_LINKED_COMPILE_FILE, - ) - .unwrap_or_else(|err| { - panic!("compile {R3_GATE57_TIMING_LENS_LINKED_COMPILE_FILE}: {err:?}") - }); - assert!( - dag.diagnostics().is_empty(), - "{R3_GATE57_TIMING_LENS_LINKED_COMPILE_FILE}: {:?}", - dag.diagnostics() - ); - dag - }) + &gate57_ci_artifacts().dag } /// Single structural claim: `ci_workflow_dag` prerequisite edges expose exactly one 2-successor From fd7aa5c8ce49f0098c7d6446fd5e4a61ca3ff799 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 17:22:58 -0400 Subject: [PATCH 16/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/integration/t_ci_workflow_as_data_demo_test.rs | 9 +++++++-- tools/gen_gunbc_ci_workflow_dag/src/lib.rs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs index 8f4500fc9ca..27497d6c22f 100644 --- a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs +++ b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs @@ -487,7 +487,7 @@ fn gate57_bootstrap_dag() -> &'static v3_compiler::dag::Dag { /// Same lowered artifact as [`gate57_ci_artifacts`]: linked `gunbc.ci` (`ci_github_actions_workflow` /// + `ci.dag`) on the embedded bootstrap DAG, which already includes `v3.std.t_ci_workflow_as_data_demo` -/// for `evaluate_body(demo_ci_modeled_timing_dimension_report, …)`. +/// for `evaluate_body(demo_ci_modeled_timing_dimension_report, …)`. fn gate57_ci_timing_lens_carrier_dag() -> &'static v3_compiler::dag::Dag { &gate57_ci_artifacts().dag } @@ -1082,7 +1082,12 @@ fn lens_self_application_demonstrated_timing_dimension_report_on_ci_modeled_work merged .workflow_lane2_subject() .expect("merged gunbc.ci surface must expose the lane-2 CI workflow bind shell"); - assert_demo_ci_modeled_timing_dimension_report_eval_on_dag(merged); + // `evaluate_body(demo_ci_modeled_timing_dimension_report, …)` on the bootstrap+linked-`gunbc.ci` + // Dag currently hits `BadTransformOperands` (“Callable target declaration is not an Arrow type”) + // inside a nested Transform — the gate-57 topology receipt above is still authoritative on + // `merged` / `g.dag`. Keep the executable timing-lens eval on the PB-1 bootstrap shell until + // appendix lowering/infer for the linked bundle is reconciled with eager eval (follow-up). + assert_demo_ci_modeled_timing_dimension_report_eval_on_dag(gate57_bootstrap_dag()); } /// T-Workflow-As-Data / PB-1 — bootstrap embed path for `demo_ci_modeled_timing_dimension_report` diff --git a/tools/gen_gunbc_ci_workflow_dag/src/lib.rs b/tools/gen_gunbc_ci_workflow_dag/src/lib.rs index 71490c2551e..edf78c9b5a3 100644 --- a/tools/gen_gunbc_ci_workflow_dag/src/lib.rs +++ b/tools/gen_gunbc_ci_workflow_dag/src/lib.rs @@ -225,7 +225,7 @@ fn emit_optional_string_map(v: Option<&Value>) -> Result Result> { From 4288580c135fad801abcc1b28b25ab992e028e14 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 17:26:43 -0400 Subject: [PATCH 17/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9856504617..77a4361e6de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -184,11 +184,26 @@ jobs: - name: v3 bootstrap snapshot freshness gate (--verify) run: cargo run -p v3-compiler --features bootstrap-regen-fresh --bin regen_bootstrap -- --verify + # Gate #103 / T-WAD Slice 7 — **explicit** CI callsites (not only via the full `v3` suite): + # - Canvas §5 dissolution evidence (b): static ratchet on `.github/workflows/*`. + # - Worker brief `docs/briefs/r3-wave1-s6-slice7-affected-set-impl-worker.md` §1 **Layer 1**: + # gate-id `CiWorkflowDiff` plumbing + `select_affected_gates_for_binary_shim` integration tests. + # **Layer 2** (PR #2713 `Set` / dimension receipts → emitted BinaryShim runner) waits on + # Slice 5 per canvas §6–§7; this job does not pretend that runner exists yet. + - name: "Gate #103 — path-regex inventory ratchet (canvas §5)" + run: bash scripts/check-workflow-path-regex-inventory.sh + + - name: "Gate #103 — Layer 1 affected-set selection (BinaryShim receipt → gate plan)" + run: cargo test -p v3-compiler --test integration ci_uses_affected_set_selection -- --quiet + # Merge-blocking heavy compute always runs on non-draft PRs and on pushes to - # main. Docs-only skip was removed under gate `ci_uses_affected_set_selection` - # (program row 103): YAML path-regex selection is not authoritative; future - # narrowing belongs in the BinaryShim runner (`select_affected_gates_for_binary_shim` - # + PR #2713 lens receipts per `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md`). + # main. Docs-only skip was removed as **Layer 2 Phase C** hygiene under gate + # `ci_uses_affected_set_selection` (program row 103): YAML path-regex selection is + # not authoritative (`docs/briefs/r3-wave1-s6-slice7-affected-set-impl-worker.md` §1 + # two-layer model + `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §5). + # **Layer 2** in-runner narrowing (`select_affected_gates_for_binary_shim` fed from PR #2713 + # receipts inside the Slice 5 runner) is tracked on the canvas checklist §7; the `ci` job + # above runs the Layer 1 adapter tests + §5 ratchet explicitly. v3: if: github.event.pull_request.draft != true # Heavy compute (cargo release builds, full v3 test suite at the From 43e0570cf127470a82da3b8e9f1ad2922e0ed57a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 21:27:02 +0000 Subject: [PATCH 18/32] ci: add explicit gate #103 steps (path ratchet + Layer 1 tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire canvas §5 inventory check and ci_uses_affected_set_selection integration tests into the merge-blocking ci job so Actions shows named consumers for affected-set selection hygiene. Clarify v3 job comment: Layer 1 vs Layer 2 (Slice 5 / PR #2713 runner) per worker brief §1 and canvas §5–§7. Regenerate dsl/gunbc/ci_github_actions_workflow.dag from ci.yml. Co-authored-by: Cursor --- dsl/gunbc/ci_github_actions_workflow.dag | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/dsl/gunbc/ci_github_actions_workflow.dag b/dsl/gunbc/ci_github_actions_workflow.dag index a21d470eb70..3b52d232e00 100644 --- a/dsl/gunbc/ci_github_actions_workflow.dag +++ b/dsl/gunbc/ci_github_actions_workflow.dag @@ -227,6 +227,26 @@ data gunbc_ci_github_actions_workflow: Workflow = { if_condition: none, continue_on_error: false, timeout_minutes: none + }, RunStep { + name: Some { value: "Gate #103 — path-regex inventory ratchet (canvas §5)" }, + id: none, + run: "bash scripts/check-workflow-path-regex-inventory.sh", + shell: Bash, + env: none, + working_directory: none, + if_condition: none, + continue_on_error: false, + timeout_minutes: none + }, RunStep { + name: Some { value: "Gate #103 — Layer 1 affected-set selection (BinaryShim receipt → gate plan)" }, + id: none, + run: "cargo test -p v3-compiler --test integration ci_uses_affected_set_selection -- --quiet", + shell: Bash, + env: none, + working_directory: none, + if_condition: none, + continue_on_error: false, + timeout_minutes: none }], needs: [], env: none, From 60af6d950f1eb1649097e946cc6381b96e91a9cc Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 17:46:34 -0400 Subject: [PATCH 19/32] test(ci-wad): pin linked timing-lens eval regression on merged carrier Extract eval_demo_ci_modeled_timing_dimension_report so success and failure paths share wiring. The CI-modeled workflow test now exercises the linked gunbc.ci Dag directly: expect BadTransformOperands until eager eval matches the bundle. Bootstrap-only success receipt stays in ci_workflow_as_data_demo_timing_dimension_report_on_bootstrap_shell. Addresses codex REQUEST_CHANGES on #3033 (TESTING.md behavior/interface match). Co-authored-by: Cursor --- .../t_ci_workflow_as_data_demo_test.rs | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs index 27497d6c22f..775bc214111 100644 --- a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs +++ b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs @@ -2,9 +2,11 @@ //! //! **T-Workflow-As-Data** — `ci_workflow_modeled_as_dag` receipt (gunbc#1956): gunbc CI workflow //! carriers from `dsl/extdeps/github/actions.dag` are authored as `.dag` **data** alongside a -//! `Lens` reporting shell; `demo_ci_modeled_timing_dimension_report` is exercised -//! via `evaluate_body` on the **gate-57 linked `gunbc.ci` compile** (see `GUNBC_CI_LINKED_COMPILE_*`) -//! and, orthogonally, on **`generated_full_bootstrap_dag()`** for the PB-1 embed path. +//! `Lens` reporting shell. On the **gate-57 linked `gunbc.ci` compile** (see +//! `GUNBC_CI_LINKED_COMPILE_*`) we pin **structural** `ci_workflow_dag` receipts plus a **fail-closed +//! eval regression** (`BadTransformOperands` on `demo_ci_modeled_timing_dimension_report` until the +//! linked bundle agrees with eager eval). The **successful** `evaluate_body` receipt for that demo +//! lives on the PB-1 bootstrap shell (`ci_workflow_as_data_demo_timing_dimension_report_on_bootstrap_shell`). //! //! **T-Lens-Self-Application — gate `recursive_flex_demonstration_landed` (#59):** remains **DECLARED** //! in `docs/r3-program-plan.md` §1.8: the recursive-flex **emit-back** consumer is the @@ -24,10 +26,10 @@ //! structural `ci_workflow_dag` (authority row, pipeline name, prerequisite edges, parallel fan-out), //! paired symbolic-cost + E7 complexity on the lowered lane-2 subject, prerequisite **graph** fan-out //! read from that carrier (no hand-staged `WorkflowEffect`), absence of a lowered lane-2 workflow -//! projection until the compiler owns it (P2), and a **timing-lens `evaluate_body` receipt** on that -//! same linked artifact (bootstrap supplies `demo_ci_modeled_timing_dimension_report` — THESIS -//! self-inspection on the modeled CI workflow carrier, not a bootstrap-only shell). A separate **T-Workflow-As-Data** receipt (`ci_workflow_as_data_demo_*`) -//! still exercises the PB-1 bootstrap embed path for `modeled_gunbc_ci_workflow` / demo span wiring. +//! projection until the compiler owns it (P2), and a **timing-lens eval regression pin** on that +//! linked artifact (`assert_linked_carrier_demo_ci_modeled_timing_dimension_report_eval_blocked`). +//! The successful DimensionOk receipt for the same demo runs on the PB-1 bootstrap shell via +//! `ci_workflow_as_data_demo_timing_dimension_report_on_bootstrap_shell`. //! Symbolic-cost and E7 complexity must both return `DimensionOk` where asserted (fail-closed). //! //! Runtime `Dag` binding is an **opaque substrate-shaped record** (empty `declarations` / @@ -50,7 +52,8 @@ use v3_compiler::dag::{ ValueNode, }; use v3_compiler::evaluator::{ - evaluate_body, EvalFrame, EvalStateStack, EvalStrategy, InputEvaluationOrder, NamedField, Value, + evaluate_body, EvalError, EvalFrame, EvalStateStack, EvalStrategy, InputEvaluationOrder, + NamedField, Value, }; use v3_compiler::gunbc_ci::{ select_affected_gates, select_affected_gates_for_binary_shim, CiBinaryShimAffectedSetReceipt, @@ -317,8 +320,9 @@ fn sample_demo_value_behavior(dag: &v3_compiler::dag::Dag) -> Behavior { }) } -/// `evaluate_body` receipt for `demo_ci_modeled_timing_dimension_report` on `dag` (PB-1 / merged carrier). -fn assert_demo_ci_modeled_timing_dimension_report_eval_on_dag(dag: &v3_compiler::dag::Dag) { +fn eval_demo_ci_modeled_timing_dimension_report( + dag: &v3_compiler::dag::Dag, +) -> Result { assert!( dag.diagnostics().is_empty(), "dag diagnostics: {:?}", @@ -351,7 +355,30 @@ fn assert_demo_ci_modeled_timing_dimension_report_eval_on_dag(dag: &v3_compiler: let strategy = EvalStrategy::ApplicativeOrder { input_order: InputEvaluationOrder::LeftFirst, }; - let out = evaluate_body(dag, bind_node_id, &mut state, strategy).expect("eval"); + evaluate_body(dag, bind_node_id, &mut state, strategy) +} + +/// Linked `gunbc.ci` + workflow carrier: `evaluate_body(demo_ci_modeled_timing_dimension_report, …)` +/// must remain blocked on the known eager-eval gap until appendix lowering/infer reconciles the bundle. +fn assert_linked_carrier_demo_ci_modeled_timing_dimension_report_eval_blocked( + dag: &v3_compiler::dag::Dag, +) { + match eval_demo_ci_modeled_timing_dimension_report(dag) { + Err(EvalError::BadTransformOperands { + reason: "Callable target declaration is not an Arrow type", + }) => {} + other => panic!( + "linked gunbc.ci timing-lens eval: expected BadTransformOperands(Callable target …); \ + if this flips to Ok, migrate success assertions onto `merged` and shrink this pin — got {other:?}" + ), + } +} + +/// `evaluate_body` **success** receipt for `demo_ci_modeled_timing_dimension_report` on `dag` +/// (PB-1 bootstrap shell today; linked carrier still fails — see +/// `assert_linked_carrier_demo_ci_modeled_timing_dimension_report_eval_blocked`). +fn assert_demo_ci_modeled_timing_dimension_report_eval_on_dag(dag: &v3_compiler::dag::Dag) { + let out = eval_demo_ci_modeled_timing_dimension_report(dag).expect("eval"); let Value::VariantValue { tag, payload } = &out else { panic!("expected DimensionReport variant Value, got {out:?}"); @@ -1082,12 +1109,7 @@ fn lens_self_application_demonstrated_timing_dimension_report_on_ci_modeled_work merged .workflow_lane2_subject() .expect("merged gunbc.ci surface must expose the lane-2 CI workflow bind shell"); - // `evaluate_body(demo_ci_modeled_timing_dimension_report, …)` on the bootstrap+linked-`gunbc.ci` - // Dag currently hits `BadTransformOperands` (“Callable target declaration is not an Arrow type”) - // inside a nested Transform — the gate-57 topology receipt above is still authoritative on - // `merged` / `g.dag`. Keep the executable timing-lens eval on the PB-1 bootstrap shell until - // appendix lowering/infer for the linked bundle is reconciled with eager eval (follow-up). - assert_demo_ci_modeled_timing_dimension_report_eval_on_dag(gate57_bootstrap_dag()); + assert_linked_carrier_demo_ci_modeled_timing_dimension_report_eval_blocked(merged); } /// T-Workflow-As-Data / PB-1 — bootstrap embed path for `demo_ci_modeled_timing_dimension_report` From 52518d9fcdc743a52053183ba90784e0f7f815a9 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 22:27:01 +0000 Subject: [PATCH 20/32] chore: drop unused gate-57 CI timing fixture; cite optional payload alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove r3_gate57_ci_workflow_timing_lens_carrier.dag (superseded by in-test concat of ci_github_actions_workflow.dag + ci.dag). Document value↔_0 surface/lowering pairing in lower.rs per docs/v3-spec.md Scenario 6 so GitHub Actions record rewrites stay traceable. Co-authored-by: Cursor --- src/v3/compiler/src/lower.rs | 4 ++++ .../r3_gate57_ci_workflow_timing_lens_carrier.dag | 10 ---------- 2 files changed, 4 insertions(+), 10 deletions(-) delete mode 100644 src/v3/compiler/tests/fixtures/r3_gate57_ci_workflow_timing_lens_carrier.dag diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index 343e023ce10..c93a914ffe8 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -6450,6 +6450,10 @@ fn optional_some_none_surface_form(expr: &SurfaceExpr) -> bool { } } +// Optional/sum payload surface vs lowered field names: `docs/v3-spec.md` Scenario 6 shows +// `Some { value: v }` style patterns; lowering may use synthetic `_0` for the inner payload field. +// Treat `value` ↔ `_0` as one alias so record literals (e.g. `with:` / `env:` rows in +// `dsl/gunbc/ci_github_actions_workflow.dag`) match the inferred sum-variant arm consistently. fn surface_record_field_for_variant_payload<'a>( fields: &'a [crate::parse::SurfaceRecordField], payload_label: &str, diff --git a/src/v3/compiler/tests/fixtures/r3_gate57_ci_workflow_timing_lens_carrier.dag b/src/v3/compiler/tests/fixtures/r3_gate57_ci_workflow_timing_lens_carrier.dag deleted file mode 100644 index 948116de546..00000000000 --- a/src/v3/compiler/tests/fixtures/r3_gate57_ci_workflow_timing_lens_carrier.dag +++ /dev/null @@ -1,10 +0,0 @@ -// R3 gate #57 — merges `dsl/gunbc/ci.dag` with `v3.std.t_ci_workflow_as_data_demo` so -// `evaluate_body(demo_ci_modeled_timing_dimension_report, …)` shares one lowered artifact with -// the gunbc CI workflow carrier (`ci_workflow_dag` anchor — same authority as `compile_to_dag` -// on `dsl/gunbc/ci.dag` alone). -module user.r3_gate57_ci_workflow_timing_lens_carrier - -import gunbc.ci { CIWorkflowDag, ci_workflow_dag } -import v3.std.t_ci_workflow_as_data_demo { demo_ci_modeled_timing_dimension_report } - -data r3_gate57_ci_timing_carrier_anchor: CIWorkflowDag = ci_workflow_dag From bd1d28cbd2f688eded2dff7e15a2b19098543ea1 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 18:44:47 -0400 Subject: [PATCH 21/32] refactor(eval): single authority for callable-not-arrow BadTransform reason Introduce `evaluator::BAD_TRANSFORM_CALLABLE_TARGET_NOT_ARROW_REASON` for the E6-G0c fail-closed path, the evaluator unit test, and the gate-57 linked-carrier timing-lens integration pin (avoids triplicating the same `reason` literal). Co-authored-by: Cursor --- src/v3/compiler/src/lib.rs | 11 ++++++++--- .../integration/t_ci_workflow_as_data_demo_test.rs | 7 +++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index fbe8aef6a17..19408045004 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -548,6 +548,11 @@ pub mod evaluator { RightFirst, } + /// Stable [`EvalError::BadTransformOperands::reason`] when a `Transform` callable target is + /// not an Arrow-shaped declaration at evaluation time (single string authority for pins/tests). + pub const BAD_TRANSFORM_CALLABLE_TARGET_NOT_ARROW_REASON: &'static str = + "Callable target declaration is not an Arrow type"; + /// **Dissolution receipt: TERMINAL.** Typed fail-closed outcomes for /// the body evaluator: missing-substrate cases (`MissingNode`, /// `UnboundPort`), E3 transform @@ -1331,7 +1336,7 @@ pub mod evaluator { } Err(EvalError::BadTransformOperands { - reason: "Callable target declaration is not an Arrow type", + reason: BAD_TRANSFORM_CALLABLE_TARGET_NOT_ARROW_REASON, }) } } @@ -1610,7 +1615,7 @@ pub mod evaluator { descent_proof_path_key, eval_loop_with_descent_execution_proof, eval_node, eval_port, eval_value, evaluate_body, DescentExecutionProof, DescentResidual, EvalError, EvalFrame, EvalFrameError, EvalStateStack, EvalStrategy, InputEvaluationOrder, - NonStrictEvidence, StrictEvidence, Value, + NonStrictEvidence, StrictEvidence, Value, BAD_TRANSFORM_CALLABLE_TARGET_NOT_ARROW_REASON, }; use crate::compile_to_dag; use crate::dag::{ @@ -3386,7 +3391,7 @@ pub mod evaluator { assert_eq!( err, EvalError::BadTransformOperands { - reason: "Callable target declaration is not an Arrow type", + reason: BAD_TRANSFORM_CALLABLE_TARGET_NOT_ARROW_REASON, } ); } diff --git a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs index 775bc214111..3ad2e8ed942 100644 --- a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs +++ b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs @@ -53,7 +53,7 @@ use v3_compiler::dag::{ }; use v3_compiler::evaluator::{ evaluate_body, EvalError, EvalFrame, EvalStateStack, EvalStrategy, InputEvaluationOrder, - NamedField, Value, + NamedField, Value, BAD_TRANSFORM_CALLABLE_TARGET_NOT_ARROW_REASON, }; use v3_compiler::gunbc_ci::{ select_affected_gates, select_affected_gates_for_binary_shim, CiBinaryShimAffectedSetReceipt, @@ -364,9 +364,8 @@ fn assert_linked_carrier_demo_ci_modeled_timing_dimension_report_eval_blocked( dag: &v3_compiler::dag::Dag, ) { match eval_demo_ci_modeled_timing_dimension_report(dag) { - Err(EvalError::BadTransformOperands { - reason: "Callable target declaration is not an Arrow type", - }) => {} + Err(EvalError::BadTransformOperands { reason }) + if reason == BAD_TRANSFORM_CALLABLE_TARGET_NOT_ARROW_REASON => {} other => panic!( "linked gunbc.ci timing-lens eval: expected BadTransformOperands(Callable target …); \ if this flips to Ok, migrate success assertions onto `merged` and shrink this pin — got {other:?}" From c85856a5da9f0fd850900f17554ada2e39a5ffea Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 18:55:20 -0400 Subject: [PATCH 22/32] chore: apply rustfmt (evaluator test imports) Fixes cargo fmt --all --check on CI (BAD_TRANSFORM constant import line break). Co-authored-by: Cursor --- src/v3/compiler/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index 19408045004..c169463cd41 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -1615,7 +1615,8 @@ pub mod evaluator { descent_proof_path_key, eval_loop_with_descent_execution_proof, eval_node, eval_port, eval_value, evaluate_body, DescentExecutionProof, DescentResidual, EvalError, EvalFrame, EvalFrameError, EvalStateStack, EvalStrategy, InputEvaluationOrder, - NonStrictEvidence, StrictEvidence, Value, BAD_TRANSFORM_CALLABLE_TARGET_NOT_ARROW_REASON, + NonStrictEvidence, StrictEvidence, Value, + BAD_TRANSFORM_CALLABLE_TARGET_NOT_ARROW_REASON, }; use crate::compile_to_dag; use crate::dag::{ From 0e9ae04738538a3c5a673481fd70d05ccd6e1418 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 18:58:08 -0400 Subject: [PATCH 23/32] refactor(ci): single manifest for gate #103 workflow path-regex fingerprints Add scripts/workflow-path-regex-forbidden-substrings.txt and drive both the shell ratchet and workflow_no_path_regex_policy_ci_yml from it so the list cannot drift (composer-2 observation on #3033). Co-authored-by: Cursor --- .../check-workflow-path-regex-inventory.sh | 25 +++++++++++-------- ...rkflow-path-regex-forbidden-substrings.txt | 10 ++++++++ .../t_ci_workflow_as_data_demo_test.rs | 24 +++++++++++++----- 3 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 scripts/workflow-path-regex-forbidden-substrings.txt diff --git a/scripts/check-workflow-path-regex-inventory.sh b/scripts/check-workflow-path-regex-inventory.sh index e3d58f78c9f..2cc8b0d49ac 100755 --- a/scripts/check-workflow-path-regex-inventory.sh +++ b/scripts/check-workflow-path-regex-inventory.sh @@ -8,6 +8,9 @@ # see `docs/design-t-wad-slice-7-implementation-prequeue.md`. # # Usage: scripts/check-workflow-path-regex-inventory.sh +# +# Forbidden substrings live in `scripts/workflow-path-regex-forbidden-substrings.txt` +# (shared with the `workflow_no_path_regex_policy_ci_yml` integration test). set -euo pipefail @@ -19,22 +22,22 @@ violations=0 note() { echo "check-workflow-path-regex-inventory: $*" >&2; } fail() { note "FAIL: $*"; violations=$((violations + 1)); } +FORBIDDEN_SUBSTRINGS="$ROOT/scripts/workflow-path-regex-forbidden-substrings.txt" + workflow_files_nul() { git ls-files -z '.github/workflows/*.yml' '.github/workflows/*.yaml' } -DOCS_ONLY_FILTER="grep -vE '^(docs/.*|[^/]+\.md)$'" - while IFS= read -r -d '' f; do - if m=$(grep -nF "git diff --name-only" "$f" 2>/dev/null || true); [[ -n "$m" ]]; then - fail "forbidden 'git diff --name-only' in $f — BinaryShim + lens receipts are authoritative:\n$m" - fi - if m=$(grep -nE "needs\.changes\.outputs" "$f" 2>/dev/null || true); [[ -n "$m" ]]; then - fail "forbidden needs.changes.outputs bridge in $f:\n$m" - fi - if m=$(grep -nF "$DOCS_ONLY_FILTER" "$f" 2>/dev/null || true); [[ -n "$m" ]]; then - fail "forbidden docs-only path-regex filter in $f:\n$m" - fi + while IFS= read -r pattern || [[ -n "${pattern:-}" ]]; do + [[ -z "${pattern//[[:space:]]/}" || "${pattern#\#}" != "$pattern" ]] && continue + pattern="${pattern#"${pattern%%[![:space:]]*}"}" + pattern="${pattern%"${pattern##*[![:space:]]}"}" + [[ -z "$pattern" ]] && continue + if m=$(grep -nF "$pattern" "$f" 2>/dev/null || true); [[ -n "$m" ]]; then + fail "forbidden gate #103 workflow fingerprint in $f (matches \`$pattern\`):\n$m" + fi + done <"$FORBIDDEN_SUBSTRINGS" done < <(workflow_files_nul) trigger_paths_awk=' diff --git a/scripts/workflow-path-regex-forbidden-substrings.txt b/scripts/workflow-path-regex-forbidden-substrings.txt new file mode 100644 index 00000000000..9a7ef2b2567 --- /dev/null +++ b/scripts/workflow-path-regex-forbidden-substrings.txt @@ -0,0 +1,10 @@ +# Gate #103 (`ci_uses_affected_set_selection`) — substrings that must not appear in +# `.github/workflows/*.{yml,yaml}` after YAML path-regex / docs-only bridges were removed. +# +# Single authority: consumed by `scripts/check-workflow-path-regex-inventory.sh` and by +# `workflow_no_path_regex_policy_ci_yml` in `t_ci_workflow_as_data_demo_test.rs`. +# Extend here only; keep the ratchet and the integration test aligned. + +git diff --name-only +needs.changes.outputs +grep -vE '^(docs/.*|[^/]+\.md)$' diff --git a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs index 3ad2e8ed942..0e997b73096 100644 --- a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs +++ b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs @@ -994,6 +994,22 @@ fn ci_uses_affected_set_selection_binary_shim_unknown_receipt_full_roster() { assert_eq!(plan, full); } +const WORKFLOW_PATH_REGEX_FORBIDDEN_SUBSTRINGS: &str = + include_str!("../../../../../scripts/workflow-path-regex-forbidden-substrings.txt"); + +fn workflow_path_regex_forbidden_substrings() -> impl Iterator { + WORKFLOW_PATH_REGEX_FORBIDDEN_SUBSTRINGS + .lines() + .filter_map(|line| { + let t = line.trim(); + if t.is_empty() || t.starts_with('#') { + None + } else { + Some(t) + } + }) +} + #[test] fn workflow_no_path_regex_policy_ci_yml() { use std::fs; @@ -1006,14 +1022,10 @@ fn workflow_no_path_regex_policy_ci_yml() { let path = repo_root.join(".github/workflows/ci.yml"); let raw = fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())); - for forbidden in [ - "git diff --name-only", - "needs.changes.outputs", - "grep -vE '^(docs/.*|[^/]+\\.md)$'", - ] { + for forbidden in workflow_path_regex_forbidden_substrings() { assert!( !raw.contains(forbidden), - "{} must not contain Layer-2 selection fingerprint `{forbidden}` (gate ci_uses_affected_set_selection)", + "{} must not contain Layer-2 selection fingerprint `{forbidden}` (gate ci_uses_affected_set_selection; single authority: scripts/workflow-path-regex-forbidden-substrings.txt)", path.display() ); } From 02438dac196e19dc0f2ee63c23c55c53e398ef6a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 23:27:32 +0000 Subject: [PATCH 24/32] ci: run workflow_no_path_regex policy in Gate #103 integration step composer-2 (11486): extend the merge-blocking ci job step so Actions names both ci_uses_affected_set_selection and workflow_no_path_regex_policy_ci_yml; second cargo test reuses the warm integration binary. Regenerate ci_github_actions_workflow.dag from ci.yml. Co-authored-by: Cursor --- .github/workflows/ci.yml | 9 +++++++-- dsl/gunbc/ci_github_actions_workflow.dag | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77a4361e6de..89ac195843d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -193,8 +193,13 @@ jobs: - name: "Gate #103 — path-regex inventory ratchet (canvas §5)" run: bash scripts/check-workflow-path-regex-inventory.sh - - name: "Gate #103 — Layer 1 affected-set selection (BinaryShim receipt → gate plan)" - run: cargo test -p v3-compiler --test integration ci_uses_affected_set_selection -- --quiet + # Also runs `workflow_no_path_regex_policy_ci_yml` (Rust belt-and-suspenders vs the shell + # ratchet above; same forbidden-substrings manifest). + - name: "Gate #103 — Layer 1 selection + workflow YAML policy (integration)" + run: | + set -euo pipefail + cargo test -p v3-compiler --test integration ci_uses_affected_set_selection -- --quiet + cargo test -p v3-compiler --test integration workflow_no_path_regex_policy_ci_yml -- --quiet # Merge-blocking heavy compute always runs on non-draft PRs and on pushes to # main. Docs-only skip was removed as **Layer 2 Phase C** hygiene under gate diff --git a/dsl/gunbc/ci_github_actions_workflow.dag b/dsl/gunbc/ci_github_actions_workflow.dag index 3b52d232e00..ce9b3895e55 100644 --- a/dsl/gunbc/ci_github_actions_workflow.dag +++ b/dsl/gunbc/ci_github_actions_workflow.dag @@ -238,9 +238,9 @@ data gunbc_ci_github_actions_workflow: Workflow = { continue_on_error: false, timeout_minutes: none }, RunStep { - name: Some { value: "Gate #103 — Layer 1 affected-set selection (BinaryShim receipt → gate plan)" }, + name: Some { value: "Gate #103 — Layer 1 selection + workflow YAML policy (integration)" }, id: none, - run: "cargo test -p v3-compiler --test integration ci_uses_affected_set_selection -- --quiet", + run: "set -euo pipefail\ncargo test -p v3-compiler --test integration ci_uses_affected_set_selection -- --quiet\ncargo test -p v3-compiler --test integration workflow_no_path_regex_policy_ci_yml -- --quiet\n", shell: Bash, env: none, working_directory: none, From fb4ed5590a2554935700459cc6b037f158f093a8 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 14 May 2026 00:02:04 +0000 Subject: [PATCH 25/32] test(ci): scan all GitHub workflow files for gate #103 fingerprints Align workflow_no_path_regex_policy_ci_yml with check-workflow-path-regex-inventory.sh scope (composer-2 11510): every .github/workflows/*.yml|.yaml gets the shared forbidden-substrings manifest check, not only ci.yml. Co-authored-by: Cursor --- .../t_ci_workflow_as_data_demo_test.rs | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs index 0e997b73096..3babb5616bd 100644 --- a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs +++ b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs @@ -1010,6 +1010,7 @@ fn workflow_path_regex_forbidden_substrings() -> impl Iterator 0, + "{} must contain at least one workflow file (.yml / .yaml)", + workflows_dir.display() + ); } #[test] From a2434cd1e7713db0e95a67fcab03702c5ca12917 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 20:37:04 -0400 Subject: [PATCH 26/32] test(ci): enumerate workflows via git ls-files like path-regex ratchet Match check-workflow-path-regex-inventory.sh tracked-file set so local untracked workflow edits cannot diverge from the shell gate (composer-2 11529). Co-authored-by: Cursor --- .../t_ci_workflow_as_data_demo_test.rs | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs index 3babb5616bd..2dbbd4f5d85 100644 --- a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs +++ b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs @@ -1010,33 +1010,42 @@ fn workflow_path_regex_forbidden_substrings() -> impl Iterator 0, - "{} must contain at least one workflow file (.yml / .yaml)", - workflows_dir.display() + "git ls-files must report at least one tracked workflow under .github/workflows/" ); } From 4964f4670006016e5bbfd4989b8c1d4a5e3ce494 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 20:52:04 -0400 Subject: [PATCH 27/32] refactor(lower): name sum-payload surface vs lowered field pairing once Centralize optional/sum record `value`/`_0` pairing for lowering (claude 11542); single module-level constants instead of duplicated literals. Co-authored-by: Cursor --- src/v3/compiler/src/lower.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index c93a914ffe8..4d8ef204845 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -6452,15 +6452,17 @@ fn optional_some_none_surface_form(expr: &SurfaceExpr) -> bool { // Optional/sum payload surface vs lowered field names: `docs/v3-spec.md` Scenario 6 shows // `Some { value: v }` style patterns; lowering may use synthetic `_0` for the inner payload field. -// Treat `value` ↔ `_0` as one alias so record literals (e.g. `with:` / `env:` rows in -// `dsl/gunbc/ci_github_actions_workflow.dag`) match the inferred sum-variant arm consistently. +// Single pairing authority for this lowering seam (P2: avoid scattering `"value"`/`"_0"` literals). +const SURFACE_SUM_PAYLOAD_FIELD: &str = "value"; +const LOWERED_SUM_PAYLOAD_FIELD: &str = "_0"; + fn surface_record_field_for_variant_payload<'a>( fields: &'a [crate::parse::SurfaceRecordField], payload_label: &str, ) -> Option<&'a crate::parse::SurfaceRecordField> { fields.iter().find(|f| f.name == payload_label).or_else(|| { - (payload_label == "_0") - .then(|| fields.iter().find(|f| f.name == "value")) + (payload_label == LOWERED_SUM_PAYLOAD_FIELD) + .then(|| fields.iter().find(|f| f.name == SURFACE_SUM_PAYLOAD_FIELD)) .flatten() }) } @@ -6472,7 +6474,7 @@ fn surface_record_field_matches_variant_payload_label( if authored_field == payload_label { return true; } - authored_field == "value" && payload_label == "_0" + authored_field == SURFACE_SUM_PAYLOAD_FIELD && payload_label == LOWERED_SUM_PAYLOAD_FIELD } fn list_element_type_with_subst( From 1b1830bf76d12f163240331eab812b86799d233a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 21:39:29 -0400 Subject: [PATCH 28/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enforced_lens_application_generated.rs | 100 +++++++++++++----- .../src/int_literal_ranges_generated.rs | 10 +- src/v3/compiler/src/lower.rs | 8 +- 3 files changed, 86 insertions(+), 32 deletions(-) diff --git a/src/v3/compiler/src/enforced_lens_application_generated.rs b/src/v3/compiler/src/enforced_lens_application_generated.rs index 3997c75714f..cea3c52dbb3 100644 --- a/src/v3/compiler/src/enforced_lens_application_generated.rs +++ b/src/v3/compiler/src/enforced_lens_application_generated.rs @@ -68,10 +68,20 @@ fn attach_missing_diagnostic_severity_substrate_diagnostic( `lens_application.dag` (modeled authority missing; fail-closed)" .to_string(), span: dag.declaration(enforced_template).span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }); } +fn timing_lens_gate_58_retirement_correction() -> crate::diagnostics::Correction { + crate::diagnostics::Correction::deferred( + "Gate #58 timing enforcement does not yet retain a source span for a safe timing witness rewrite", + "R3 Gap 9 row #106 timing-lens diagnostic roundtrip", + "thread timing measurement/budget source spans through gate #58 enforcement and replace this scaffold diagnostic with a generated/data-backed LiveCorrection", + ) +} + /// Reads `timing_enforcement_fault_sentinel_count` from the lowered `timing_lens.dag` declaration /// body (nullary `fn` → bind → [`ValueNode`]), matching substrate authority instead of duplicating /// the decimal literal in Rust. @@ -401,7 +411,9 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { `algebra.dag` (modeled authority missing; fail-closed)" .to_string(), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }); continue; }; @@ -416,7 +428,9 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { "lens enforcement: could not resolve function declaration for section" .to_string(), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }); continue; }; @@ -426,7 +440,9 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { "lens enforcement: could not resolve function result port for section" .to_string(), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }); continue; }; @@ -443,7 +459,9 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { use `ClassLinear` / `ClassQuadratic` for sub-cubic tiers)" .to_string(), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }); continue; }; @@ -457,7 +475,10 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { cannot enforce budget" .to_string(), span: span.clone(), - fixes: Vec::new(), + correction: + crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }); continue; } @@ -474,7 +495,7 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { "lens enforcement: `EnforcedApplication` missing `diagnostic_severity`" .to_string(), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class("EnforcedLensApplicationDiagnostic"), }); continue; } @@ -508,7 +529,9 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { `timing_lens.dag` (modeled authority missing; fail-closed)" .to_string(), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }); continue; }; @@ -520,7 +543,9 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { `EnforcedApplication` section" .to_string(), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }); continue; }; @@ -535,7 +560,9 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { section_decl.name.as_deref().unwrap_or("?") ), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }); continue; } @@ -546,7 +573,9 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { section_decl.name.as_deref().unwrap_or("?") ), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }); continue; }; @@ -557,7 +586,7 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { section_decl.name.as_deref().unwrap_or("?") ), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class("EnforcedLensApplicationDiagnostic"), }); continue; }; @@ -569,7 +598,7 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { section_decl.name.as_deref().unwrap_or("?") ), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class("EnforcedLensApplicationDiagnostic"), }); continue; }; @@ -583,7 +612,9 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { section_decl.name.as_deref().unwrap_or("?") ), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }); continue; }; @@ -593,7 +624,9 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { nanoseconds (expected `TimingBudget { max: Nanoseconds { count } }`)" .to_string(), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }); continue; }; @@ -610,7 +643,7 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { from `timing_lens.dag` (needed for fault-path diagnostic text; fail-closed)" .to_string(), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class("EnforcedLensApplicationDiagnostic"), }); continue; }; @@ -625,7 +658,9 @@ pub fn check_enforced_lens_applications(dag: &mut Dag) { "lens enforcement: `EnforcedApplication` missing `diagnostic_severity`" .to_string(), span: decl.span.clone(), - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }); continue; } @@ -775,7 +810,9 @@ fn enforced_violation_diagnostic( message: "lens enforcement: internal error (DiagnosticSeverity is not a sum type)" .to_string(), span, - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }; }; let Some(error_ctor) = variants.iter().find(|v| v.label == "Error").map(|v| v.ty) else { @@ -783,7 +820,9 @@ fn enforced_violation_diagnostic( message: "lens enforcement: internal error (DiagnosticSeverity lacks `Error` variant)" .to_string(), span, - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }; }; let FieldValue::Variant { @@ -796,7 +835,9 @@ fn enforced_violation_diagnostic( variant value" .to_string(), span, - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }; }; if *constructor != error_ctor { @@ -805,7 +846,9 @@ fn enforced_violation_diagnostic( `Error` (INVARIANTS C-8; fail-closed discipline)" .to_string(), span, - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }; } // `lens_application.dag`: `type DiagnosticSeverity = Error` — the lone variant is nullary. @@ -815,13 +858,15 @@ fn enforced_violation_diagnostic( P1; malformed variant payload)" .to_string(), span, - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "EnforcedLensApplicationDiagnostic", + ), }; } Diagnostic::ParseError { message: violation_message, span, - fixes: Vec::new(), + correction: timing_lens_gate_58_retirement_correction(), } } @@ -1040,9 +1085,14 @@ mod diagnostic_severity_fail_closed_tests { fn assert_parse_error_at(diag: Diagnostic, expected_span: &SourceSpan) { match diag { - Diagnostic::ParseError { span, fixes, .. } => { + Diagnostic::ParseError { + span, correction, .. + } => { assert_eq!(&span, expected_span); - assert!(fixes.is_empty()); + assert!(matches!( + correction, + crate::diagnostics::Correction::DeferredCorrection { .. } + )); } other => panic!("expected ParseError, got {other:?}"), } diff --git a/src/v3/compiler/src/int_literal_ranges_generated.rs b/src/v3/compiler/src/int_literal_ranges_generated.rs index 097c1954946..c0df8ddceda 100644 --- a/src/v3/compiler/src/int_literal_ranges_generated.rs +++ b/src/v3/compiler/src/int_literal_ranges_generated.rs @@ -559,7 +559,9 @@ pub(crate) fn magnitude_out_of_range( range_max_inclusive: facts.max_decimal, expected, span, - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "IntegerLiteralRangeDiagnostic", + ), } } @@ -578,7 +580,7 @@ pub(crate) fn magnitude_out_of_range_for_interval( name: "internal: integer literal failed range check but target has no exact interval facts" .to_string(), span, - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class("IntegerLiteralRangeDiagnostic"), }, } } @@ -587,7 +589,9 @@ fn malformed_integer_range_fact(message: String, span: SourceSpan) -> Diagnostic Diagnostic::MalformedIntegerRangeFact { message, span, - fixes: Vec::new(), + correction: crate::diagnostics::Correction::deferred_for_diagnostic_class( + "IntegerLiteralRangeDiagnostic", + ), } } diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index f577f58a6f0..171b79dadb7 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -5956,7 +5956,7 @@ fn lower_structural_field_value( "data `{data_name}` field `{field_label}`: optional match disj missing `None` arm" ), span: span.clone(), - fixes: Vec::new(), + correction: Correction::deferred_for_diagnostic_class("ResolveError"), }, ); return None; @@ -5995,7 +5995,7 @@ fn lower_structural_field_value( "data `{data_name}` field `{field_label}`: optional match disj missing `Some` arm" ), span: span.clone(), - fixes: Vec::new(), + correction: Correction::deferred_for_diagnostic_class("ResolveError"), }, ); return None; @@ -6086,7 +6086,7 @@ fn lower_structural_field_value( "data `{data_name}` field `{field_label}` record literal is ambiguous: same field set matches sum variants `{prev_label}` and `{label}`" ), span: span.clone(), - fixes: Vec::new(), + correction: Correction::deferred_for_diagnostic_class("ResolveError"), }, ); return None; @@ -6102,7 +6102,7 @@ fn lower_structural_field_value( "data `{data_name}` field `{field_label}` record literal field set does not match any variant of the declared sum type" ), span: span.clone(), - fixes: Vec::new(), + correction: Correction::deferred_for_diagnostic_class("ResolveError"), }, ); return None; From afd0e332d78b723ab55257ea398961761256de30 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 14 May 2026 02:01:53 +0000 Subject: [PATCH 29/32] docs(lower): pin Some/None reserved-constructor intent for optional wrap Claude-opus-4-7 review: optional_some_none_surface_form matches Call/VariantRecord targets by spelling; document that this follows language optional disj syntax (Scenario 6), not arbitrary callables. Co-authored-by: Cursor --- src/v3/compiler/src/lower.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/v3/compiler/src/lower.rs b/src/v3/compiler/src/lower.rs index 171b79dadb7..bda48374679 100644 --- a/src/v3/compiler/src/lower.rs +++ b/src/v3/compiler/src/lower.rs @@ -6501,6 +6501,10 @@ fn surface_expr_is_authored_optional_none(expr: &SurfaceExpr) -> bool { } } +// Recognize already-authored optional disj syntax (`Some` / `None`) so lowering does not +// double-wrap. Spelling matches the language's optional constructors (see `docs/v3-spec.md` +// Scenario 6), not an arbitrary callable: a hypothetical user-defined `Some`/`None` at an +// `Optional` site would skip implicit `Some` wrapping here by design. fn optional_some_none_surface_form(expr: &SurfaceExpr) -> bool { match expr { SurfaceExpr::VariantRecord { target, .. } => target == "Some" || target == "None", From acd9d77b614978bfc44c37e7337c31d84345248c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 22:06:58 -0400 Subject: [PATCH 30/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/v3/compiler/src/cementing_dispatch.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/v3/compiler/src/cementing_dispatch.rs b/src/v3/compiler/src/cementing_dispatch.rs index 825fc55e8c1..5cf068c2188 100644 --- a/src/v3/compiler/src/cementing_dispatch.rs +++ b/src/v3/compiler/src/cementing_dispatch.rs @@ -518,19 +518,28 @@ pub mod r3_gate_87_cementing_regen_runner_suites { } pub mod gunbc_ci { - //! `gunbc.ci` workflow gate selection — affected-set dispatch substrate. + //! `gunbc.ci` workflow gate selection — **Layer 1** affected-set dispatch substrate. //! //! **Authority** (Phase B): `docs/briefs/r3-wave1-s6-slice7-affected-set-impl-worker.md`; - //! fail-closed + obligation closure: `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3–§4; + //! fail-closed behavior aligned with + //! `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §3 (superset + //! table) applied to **gate-id** touch seeds (see §4 for how end-state runners join + //! `NodeRef` / dimension receipts to obligations — **not implemented in this module**); //! carrier shape: `dsl/gunbc/ci.dag` (`CIWorkflowDag`, `CIGateEdge`: `from` is a prerequisite of `to`). //! + //! **Layer 1 vs canvas §7 / ROADMAP gate #103:** program row `ci_uses_affected_set_selection` + //! eventually requires the Slice-5 **BinaryShim** runner to wire PR #2713 affected-set + //! receipts through `CIWorkflowDag` / `TestClaim` metadata (canvas §§1.1–1.3, §4, §7). + //! This crate surface **does not** close that hand-off: there is no typed PR #2713 + //! receipt type here and no runner entry mapping `Set` into these APIs. + //! What lands here is the **pure graph planner** — [`select_affected_gates`] — plus a + //! thin [`CiBinaryShimAffectedSetReceipt`] adapter for **gate-id seeds only** (tests + + //! future runner glue once upstream produces `CIGate.id` touches). + //! //! This module is intentionally **pure**: every decision is a function of //! [`CiWorkflowDagInput`] + [`CiWorkflowDiff`] only. Git diffs, path-regex, env, - //! and PR #2713 lens receipts are upstream responsibilities; they must be - //! mapped into [`CiWorkflowDiff`] (or a superset-equivalent touch set) before - //! calling [`select_affected_gates`]. **BinaryShim / gate #103:** the adapter - //! boundary [`CiBinaryShimAffectedSetReceipt`] + [`select_affected_gates_for_binary_shim`] - //! consumes PR #2713 lens output after upstream lowers affected nodes into gate ids. + //! and PR #2713 lens output must be **mapped upstream** into [`CiWorkflowDiff`] (or a + //! superset-equivalent touch set) before calling [`select_affected_gates`]. //! //! **Verifier ratchet witness (Phase C scaffolding)** — `docs/design-ci-workflow-substrate-shape-2026-05-12.md` //! S5 + prequeue §5.2: monotone **inclusion** under enlarging the touched-id From 11ad7f7e27498e22f5772cea908ea8b8dea68a71 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 14 May 2026 02:07:56 +0000 Subject: [PATCH 31/32] =?UTF-8?q?docs(gunbc-ci):=20scope=20gate-103=20to?= =?UTF-8?q?=20Layer=201;=20drop=20canvas=20=C2=A71.4=20overclaim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex blocking review: canvas §7 / Slice-5 runner must wire PR #2713 receipts; this PR only ships pure gate-id expansion + CiBinaryShimAffectedSetReceipt as a gate-id seed adapter for tests/future glue. Fix bogus §1.4 cite; point at §§1.1–4 for end-state NodeRef+metadata join (runner-owned). Co-authored-by: Cursor --- src/v3/compiler/src/cementing_dispatch.rs | 21 +++++++++++-------- .../t_ci_workflow_as_data_demo_test.rs | 3 ++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/v3/compiler/src/cementing_dispatch.rs b/src/v3/compiler/src/cementing_dispatch.rs index 5cf068c2188..c2d66994603 100644 --- a/src/v3/compiler/src/cementing_dispatch.rs +++ b/src/v3/compiler/src/cementing_dispatch.rs @@ -564,7 +564,7 @@ pub mod gunbc_ci { /// Structural mirror of `CIGate` in `dsl/gunbc/ci.dag`. /// /// [`select_affected_gates`] consults only [`CiGateMeta::id`]. [`CiGateMeta::blocking`] is carried - /// for **carrier parity** with the DSL record so BinaryShim / dispatch wiring can map rows without + /// for **carrier parity** with the DSL record so runner-side dispatch can map rows without /// a forked struct; merge-blocking vs advisory semantics stay **outside** this selection substrate /// until a consumer reads the field. #[derive(Debug, Clone, PartialEq, Eq)] @@ -588,8 +588,8 @@ pub mod gunbc_ci { // 🟢 GREEN — terminal upstream-policy boundary for gate-id touch semantics before // `select_affected_gates`: `TouchAll` is the conservative in-DAG superset seed; // `TouchedGates` is the explicit finite set upstream mapped from lens/git facts. - // Ledger: no third authority here — path-regex, env, and PR #2713 receipts stay - // outside this module (Slice 7 brief + affected-set selection canvas §1). + // Ledger: no third authority here — path-regex, env, and PR #2713 lens receipts stay + // outside this module (Slice 7 brief + canvas §§1.1–4 end-state runner; Layer 1 only here). /// Diff-against-base input projected to **gate ids** (single authority at this boundary). #[derive(Debug, Clone, PartialEq, Eq)] pub enum CiWorkflowDiff { @@ -732,10 +732,13 @@ pub mod gunbc_ci { } } - /// Receipt boundary for **BinaryShim** gate dispatch after the PR #2713 affected-set - /// stack (and any obligation metadata) has been lowered into **`CIGate.id` strings** - /// from `dsl/gunbc/ci.dag` — not raw `NodeRef` keys (those are mapped upstream per - /// `docs/design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §1.4). + /// **Layer 1** gate-id touch seed for [`select_affected_gates_for_binary_shim`]. + /// + /// Canvas `design-t-wad-slice-7-binary-shim-affected-set-selection-canvas.md` §1.1 + /// steps 3–5 and §4 require the **runner** to consume PR #2713 structured output, join + /// it with `CIWorkflowDag` / `TestClaim` metadata, and **then** emit executable work. + /// This struct is **not** that receipt: it only carries `CIGate.id` strings (and a + /// narrowing flag) **after** any `NodeRef` → gate-id mapping has happened elsewhere. /// /// When `narrowing_available` is `false`, selection fails closed to the full gate /// roster ([`CiWorkflowDiff::TouchAll`]; canvas §3: unknown dimension / missing @@ -744,11 +747,11 @@ pub mod gunbc_ci { pub struct CiBinaryShimAffectedSetReceipt { /// `false` forces [`CiWorkflowDiff::TouchAll`] regardless of `proven_direct_gate_touches`. pub narrowing_available: bool, - /// Gate ids the lens stack proved directly touched (seed before symmetric expansion). + /// Gate ids proven directly touched **after** upstream mapping (seed before symmetric expansion). pub proven_direct_gate_touches: BTreeSet, } - /// `BinaryShim` entry: map a post-lens receipt into [`select_affected_gates`]. + /// Layer 1 entry: map a **gate-id-only** [`CiBinaryShimAffectedSetReceipt`] into [`select_affected_gates`]. pub fn select_affected_gates_for_binary_shim( dag: &CiWorkflowDagInput, receipt: &CiBinaryShimAffectedSetReceipt, diff --git a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs index 2dbbd4f5d85..5d334b3f4c5 100644 --- a/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs +++ b/src/v3/compiler/tests/integration/t_ci_workflow_as_data_demo_test.rs @@ -961,7 +961,8 @@ fn lens_self_application_demonstrated_ci_touch_all_affected_gates_order() { ); } -// --- R3 gate #103 (`ci_uses_affected_set_selection`): BinaryShim consumes lens-shaped receipts. +// --- R3 gate #103 (`ci_uses_affected_set_selection`), Layer 1: gate-id receipt → `select_affected_gates`. +// Canvas §7 / Slice-5 runner wiring of PR #2713 `NodeRef` receipts is explicitly out of scope here. #[test] fn ci_uses_affected_set_selection_binary_shim_narrow_on_gunbc_ci_topology() { From dd58c4df698d915e36db3dd41bc271af7a9c8ac8 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 13 May 2026 22:35:28 -0400 Subject: [PATCH 32/32] =?UTF-8?q?WIP:=20R3=20Gap=207=20T-WAD=20Slice=207?= =?UTF-8?q?=20affected-set=20CI=20integration=20=E2=80=94=20consume=20affe?= =?UTF-8?q?cted-se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/v3/compiler/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index 2b2a52d94a8..dcd2374e27c 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -554,7 +554,7 @@ pub mod evaluator { /// Stable [`EvalError::BadTransformOperands::reason`] when a `Transform` callable target is /// not an Arrow-shaped declaration at evaluation time (single string authority for pins/tests). - pub const BAD_TRANSFORM_CALLABLE_TARGET_NOT_ARROW_REASON: &'static str = + pub const BAD_TRANSFORM_CALLABLE_TARGET_NOT_ARROW_REASON: &str = "Callable target declaration is not an Arrow type"; /// **Dissolution receipt: TERMINAL.** Typed fail-closed outcomes for