From bb95002bee1c5c2b32188b49a3f03b42fde83fc9 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:30:01 -0400 Subject: [PATCH 1/3] ci: delete unread advisory workflows and the canary PR trigger video-digest-source-liveness.yml ran red on its schedule for five weeks outside ci-status; delete it with its liveness harness, its requirements file, the package.json aliases and the em-dash allowlist glob. Nothing outside the workflow used the harness. hook-utils-timing.yml ran twice with nobody reading the result; delete it with the HOOK_UTILS_TIMING-gated clock comparisons in lib/hook-utils.test.sh and the pairing and estimator helpers only they used. The load-independent engagement and read-count probes stay as the regression guards. silent-revert-canary.yml keeps its push-time canary and loses only the pull_request self-test trigger, its now-dead event gates and comments. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019mo9xaZ53JHHy4aBjgd8wK --- .../requirements-video-digest-liveness.txt | 6 - .github/workflows/ci.yml | 1 - .github/workflows/hook-utils-timing.yml | 52 -- .github/workflows/silent-revert-canary.yml | 49 +- .../video-digest-source-liveness.yml | 156 ------ docs/architecture/landscape.json | 5 +- docs/architecture/landscape.md | 2 +- docs/ci-runner-routing.md | 5 +- lib/hook-utils.test.sh | 341 +------------ .../extraction/liveness/LIVENESS.md | 71 --- .../liveness/fixtures/x-login-required.json | 5 - .../liveness/fixtures/x-metadata.json | 11 - .../liveness/fixtures/youtube-metadata.json | 11 - .../extraction/liveness/probes.json | 39 -- .../liveness/run-source-liveness.js | 458 ------------------ .../liveness/run-source-liveness.test.js | 222 --------- .../video-digest/extraction/package.json | 4 +- scripts/em-dash-purged-paths.txt | 1 - 18 files changed, 28 insertions(+), 1411 deletions(-) delete mode 100644 .github/requirements-video-digest-liveness.txt delete mode 100644 .github/workflows/hook-utils-timing.yml delete mode 100644 .github/workflows/video-digest-source-liveness.yml delete mode 100644 plugins/knowledge/skills/video-digest/extraction/liveness/LIVENESS.md delete mode 100644 plugins/knowledge/skills/video-digest/extraction/liveness/fixtures/x-login-required.json delete mode 100644 plugins/knowledge/skills/video-digest/extraction/liveness/fixtures/x-metadata.json delete mode 100644 plugins/knowledge/skills/video-digest/extraction/liveness/fixtures/youtube-metadata.json delete mode 100644 plugins/knowledge/skills/video-digest/extraction/liveness/probes.json delete mode 100755 plugins/knowledge/skills/video-digest/extraction/liveness/run-source-liveness.js delete mode 100644 plugins/knowledge/skills/video-digest/extraction/liveness/run-source-liveness.test.js diff --git a/.github/requirements-video-digest-liveness.txt b/.github/requirements-video-digest-liveness.txt deleted file mode 100644 index e33be7c5fe..0000000000 --- a/.github/requirements-video-digest-liveness.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Pinned yt-dlp for the video-digest source-liveness live-probe job. -# Exact version + wheel hash — same --require-hashes posture as -# .github/requirements-ci.txt. Bump via a reviewed PR when the floor moves. -yt-dlp==2026.8.19 \ - --hash=sha256:1d57897e94c6665a0a6f9bc54b34e584284e32c034ffab3a7df25d8f7b24eedf \ - --hash=sha256:9e213e48cea35c66b378e4447903f118f6392a5fa380a2b6d7070ec86f4e0af1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 805c9f7aa6..bc55842776 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -475,7 +475,6 @@ jobs: .github/workflows/claude-review.yml .github/workflows/claude-security-review.yml .github/workflows/dependabot-miro-bundle.yml - .github/workflows/hook-utils-timing.yml .github/workflows/silent-revert-canary.yml - name: Verify shebang files are executable diff --git a/.github/workflows/hook-utils-timing.yml b/.github/workflows/hook-utils-timing.yml deleted file mode 100644 index d288ca8599..0000000000 --- a/.github/workflows/hook-utils-timing.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: hook-utils-timing - -# Weekly wall-clock comparisons for lib/hook-utils.sh's bounded stdin read. -# lib/hook-utils.test.sh carries two interleaved-pair clock comparisons (the -# late-EOF window count and the stall overshoot) that each wait out real read -# bounds: about 80 s per run on a hosted runner and identical on both operating -# systems, while the suite's own comments call them advisory next to the -# load-independent probes that guard the same regressions (#2105). Every -# ordinary run of the suite therefore reports them as deferred, and this lane -# is where they actually run: HOOK_UTILS_TIMING=1 enables them, once a week on -# each platform, off the required merge gate. Advisory by construction: nothing -# aggregates this workflow into ci-status, and a flake here blocks no merge. -# -# Two literal jobs rather than a matrix so the runner policy reads pinned -# labels (never `*-latest`), the same rule ci.yml's lanes follow. -on: - schedule: - - cron: "17 6 * * 1" # Mondays 06:17 UTC - workflow_dispatch: - -permissions: - contents: read - -jobs: - linux: - runs-on: ubuntu-24.04 - timeout-minutes: 20 - steps: - - name: Check out - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Run the shared lib suite with the clock comparisons enabled - env: - HOOK_UTILS_TIMING: "1" - run: bash lib/hook-utils.test.sh - - windows: - runs-on: windows-2025 - timeout-minutes: 25 - defaults: - run: - shell: bash - steps: - - name: Check out - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Run the shared lib suite with the clock comparisons enabled on Windows - env: - HOOK_UTILS_TIMING: "1" - run: bash lib/hook-utils.test.sh diff --git a/.github/workflows/silent-revert-canary.yml b/.github/workflows/silent-revert-canary.yml index 23dcc2bafa..59b1335aac 100644 --- a/.github/workflows/silent-revert-canary.yml +++ b/.github/workflows/silent-revert-canary.yml @@ -12,13 +12,10 @@ name: silent-revert-canary # # DETECTION, NOT PREVENTION -- and deliberately so. # -# * Scanning runs on `push` to main only. There IS a `pull_request` trigger -# below, but it only exercises the detector against its own tests and its -# recorded corpus (see the note above `on:`); the range-resolution and scan -# steps are both gated `if: github.event_name != 'pull_request'`, so no PR -# is ever inspected for silent reverts. By the time the canary speaks, the -# merge has happened; the value is that a human learns within minutes -# instead of during a from-scratch content audit weeks later. +# * Scanning runs on `push` to main only, so no PR is ever inspected for +# silent reverts. By the time the canary speaks, the merge has happened; +# the value is that a human learns within minutes instead of during a +# from-scratch content audit weeks later. # * It is NOT in ci.yml and is NOT wired into that workflow's `ci-status` # aggregate, which is the single required check the org ci-gate ruleset # keys on. Adding it there would make a detection heuristic able to block @@ -38,33 +35,9 @@ name: silent-revert-canary # exactly the false-green this lane exists to remove. Pushes to main are # infrequent enough that letting every run finish costs nothing worth saving. -# The `pull_request` trigger exercises the detector against its OWN unit tests -# and its recorded incident corpus, never against the PR. The range-resolution -# and scan steps are gated off for PR events below, so nothing on a PR ever -# inspects that PR for silent reverts -- detection stays post-merge, per the -# design note above. What a PR gets is ordinary coverage of a shipped script, -# scoped by `paths` to the canary's own files so it is inert on every other PR. -# -# Without it the detector would ship untested until the next push to main, and -# the specific way this script can break is a false GREEN: if the blame parser -# stops matching (awk dialects differ between the runner's mawk and a -# developer's gawk), attribution yields nothing and every commit reports `ok`. -# A canary whose failure mode is silent success is the exact thing #2691 is -# about, so it gets tested before it lands, not after. -# -# This does not make the lane a merge gate. It is not in ci.yml and not in that -# workflow's `ci-status` aggregate, which is the single required check the org -# ci-gate ruleset keys on, so nothing here can block a merge. on: push: branches: [main] - pull_request: - paths: - - 'scripts/check-silent-revert.sh' - - 'scripts/check-silent-revert.test.sh' - - 'scripts/silent-revert-incidents.txt' - - 'scripts/silent-revert-acknowledged.txt' - - '.github/workflows/silent-revert-canary.yml' workflow_dispatch: permissions: @@ -109,12 +82,8 @@ jobs: # workflow_dispatch; in both cases fall back to the head commit and SAY # SO, rather than reporting a clean scan of nothing. Values arrive through # env, never interpolated into the script body. - # Detection is post-merge only. On a pull_request event the two steps - # above have already proven the detector works; scanning stops here so the - # lane never inspects a PR and never has an opinion about merging it. - name: Resolve the pushed range id: range - if: github.event_name != 'pull_request' env: EVENT_BEFORE: ${{ github.event.before }} EVENT_AFTER: ${{ github.event.after }} @@ -141,7 +110,6 @@ jobs: fi - name: Scan the merged commits for silent reverts - if: github.event_name != 'pull_request' env: SCAN_MODE: ${{ steps.range.outputs.mode }} SCAN_TARGET: ${{ steps.range.outputs.target }} @@ -181,14 +149,9 @@ jobs: # skip the restoration answer. It excludes cancellation, so a cancelled # run still reports nothing rather than a result it did not earn. # - # UNGATED on the event, exactly like the self-test and the replay. The + # Runs on every push, exactly like the self-test and the replay. The # failure that produced #2828 is "nobody thought to check", so an - # on-demand mode would reproduce it. Running on pull_request events too is - # not a merge gate: this workflow is not in ci.yml and not in that - # workflow's `ci-status` aggregate, which is the single required check the - # org ci-gate ruleset keys on -- and it is `paths`-scoped to the canary's - # own files, so it is inert on every other PR. What it buys is that a PR - # editing this corpus proves its markers resolve before the merge. + # on-demand mode would reproduce it. # # No rev argument: it resolves against the checked-out tree, which is the # question worth asking here -- is the content on main RIGHT NOW. The diff --git a/.github/workflows/video-digest-source-liveness.yml b/.github/workflows/video-digest-source-liveness.yml deleted file mode 100644 index 4794719791..0000000000 --- a/.github/workflows/video-digest-source-liveness.yml +++ /dev/null @@ -1,156 +0,0 @@ -name: video-digest-source-liveness - -# Advisory source-adapter liveness lane for the video-digest pipeline (#2797). -# Offline conformance stays the CI merge gate; this lane only detects live -# upstream drift (yt-dlp / YouTube / X). It is deliberately NOT in ci.yml and -# NOT in the ci-status aggregate — a red run annotates the job, writes a step -# summary, and uploads the probe report as an artifact. It never opens or -# updates GitHub issues (drain forbids filing). -# -# pull_request: hermetic --offline self-test only (no network). -# schedule / workflow_dispatch: live yt-dlp probes + annotate/upload on failure. - -on: - schedule: - # Weekly Monday 07:17 UTC. - - cron: '17 7 * * 1' - workflow_dispatch: - pull_request: - paths: - - 'plugins/knowledge/skills/video-digest/extraction/liveness/**' - - '.github/workflows/video-digest-source-liveness.yml' - -permissions: - contents: read - -concurrency: - group: video-digest-source-liveness-${{ github.event_name }}-${{ github.ref }} - cancel-in-progress: false - -jobs: - offline-self-test: - name: Offline liveness self-test - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - name: Check out - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version-file: .node-version - cache: npm - cache-dependency-path: plugins/knowledge/skills/video-digest/extraction/package-lock.json - - - name: Install extraction deps - run: bash plugins/knowledge/skills/video-digest/scripts/run-tests.sh install - - - name: Replay offline fixtures - run: > - node plugins/knowledge/skills/video-digest/extraction/liveness/run-source-liveness.js - --offline - - - name: Unit-test the liveness harness - working-directory: plugins/knowledge/skills/video-digest/extraction - run: npx vitest run liveness/run-source-liveness.test.js - - live-probe: - name: Live source-adapter probes - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-24.04 - timeout-minutes: 30 - permissions: - contents: read - steps: - - name: Check out - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version-file: .node-version - cache: npm - cache-dependency-path: plugins/knowledge/skills/video-digest/extraction/package-lock.json - - - name: Install extraction deps - run: bash plugins/knowledge/skills/video-digest/scripts/run-tests.sh install - - - name: Install yt-dlp - run: | - set -euo pipefail - python3 -m pip install --user --only-binary=:all: --require-hashes \ - --requirement .github/requirements-video-digest-liveness.txt - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - yt-dlp --version - - - name: Materialize yt-dlp cookies file (optional) - id: cookies - env: - COOKIES_SECRET: ${{ secrets.VIDEO_DIGEST_YT_DLP_COOKIES_FILE }} - run: | - set -euo pipefail - if [[ -z "${COOKIES_SECRET}" ]]; then - echo "path=" >> "${GITHUB_OUTPUT:?}" - exit 0 - fi - cookies_file="$(mktemp)" - # Secret holds Netscape cookie-jar contents, not a host path. - printf '%s\n' "${COOKIES_SECRET}" > "${cookies_file}" - chmod 600 "${cookies_file}" - echo "path=${cookies_file}" >> "${GITHUB_OUTPUT:?}" - - - name: Probe live sources - id: probe - env: - VIDEO_DIGEST_YT_DLP_COOKIES_FILE: ${{ steps.cookies.outputs.path }} - run: | - set -euo pipefail - set +e - node plugins/knowledge/skills/video-digest/extraction/liveness/run-source-liveness.js --live \ - | tee liveness-report.txt - status=${PIPESTATUS[0]} - set -e - if ((status == 0)); then - echo "drift_detected=false" >> "$GITHUB_OUTPUT" - else - echo "drift_detected=true" >> "$GITHUB_OUTPUT" - fi - exit "$status" - - - name: Annotate drift and write job summary - if: failure() && steps.probe.outputs.drift_detected == 'true' - run: | - set -euo pipefail - fence=$(printf '\140\140\140') - { - echo "## Video-digest source-adapter liveness drift" - echo - echo "Scheduled/live probe failed on \`${GITHUB_SHA}\` (run" - echo "[${GITHUB_RUN_ID}](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})." - echo - echo "Offline conformance remains the merge gate. This run does not" - echo "open or update GitHub issues — see the uploaded" - echo "liveness-report artifact and the annotations on this job." - echo "Owner doc:" - echo "plugins/knowledge/skills/video-digest/extraction/liveness/LIVENESS.md" - echo - echo "### Probe output" - echo - echo "${fence}" - cat liveness-report.txt - echo "${fence}" - } | tee -a "${GITHUB_STEP_SUMMARY}" - echo "::error::Video-digest source-adapter liveness drift detected on ${GITHUB_SHA}. See job summary and liveness-report artifact (no tracking issue filed)." - - - name: Upload liveness report artifact - if: failure() && steps.probe.outputs.drift_detected == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: liveness-report - path: liveness-report.txt - if-no-files-found: error diff --git a/docs/architecture/landscape.json b/docs/architecture/landscape.json index 2fdebc9f95..806b1224d1 100644 --- a/docs/architecture/landscape.json +++ b/docs/architecture/landscape.json @@ -84,10 +84,9 @@ {"from":"claude-code-plugins","to":"anthropics/claude-plugins-official","type":"installs-plugin","relation":"external","count":1,"files":["plugins/playgrounds/skills/use/SKILL.md"]}, {"from":"claude-code-plugins","to":"dometrain/mcp","type":"installs-plugin","relation":"external","count":1,"files":["plugins/dometrain/README.md"]}, {"from":"claude-code-plugins","to":"trailofbits/skills","type":"installs-plugin","relation":"external","count":1,"files":["plugins/review/skills/audit-enforceability/SKILL.md"]}, - {"from":"claude-code-plugins","to":"actions/checkout","type":"uses-workflow","relation":"external","count":15,"files":[".github/workflows/ci.yml",".github/workflows/claude-review.yml",".github/workflows/claude-security-review.yml",".github/workflows/dependabot-miro-bundle.yml",".github/workflows/hook-utils-timing.yml"]}, - {"from":"claude-code-plugins","to":"actions/setup-node","type":"uses-workflow","relation":"external","count":5,"files":[".github/workflows/ci.yml",".github/workflows/dependabot-miro-bundle.yml",".github/workflows/video-digest-source-liveness.yml"]}, + {"from":"claude-code-plugins","to":"actions/checkout","type":"uses-workflow","relation":"external","count":11,"files":[".github/workflows/ci.yml",".github/workflows/claude-review.yml",".github/workflows/claude-security-review.yml",".github/workflows/dependabot-miro-bundle.yml",".github/workflows/managed-files-guard.yml"]}, + {"from":"claude-code-plugins","to":"actions/setup-node","type":"uses-workflow","relation":"external","count":3,"files":[".github/workflows/ci.yml",".github/workflows/dependabot-miro-bundle.yml"]}, {"from":"claude-code-plugins","to":"actions/setup-python","type":"uses-workflow","relation":"external","count":3,"files":[".github/workflows/ci.yml"]}, - {"from":"claude-code-plugins","to":"actions/upload-artifact","type":"uses-workflow","relation":"external","count":1,"files":[".github/workflows/video-digest-source-liveness.yml"]}, {"from":"claude-code-plugins","to":"melodic-software/ci-workflows","type":"uses-workflow","relation":"internal","count":23,"files":[".github/workflows/ci.yml",".github/workflows/claude-review.yml",".github/workflows/claude-security-review.yml",".github/workflows/issue-triage-label.yml",".github/workflows/managed-files-guard.yml"]} ] } diff --git a/docs/architecture/landscape.md b/docs/architecture/landscape.md index b00a3e1ee2..5430d8c753 100644 --- a/docs/architecture/landscape.md +++ b/docs/architecture/landscape.md @@ -43,7 +43,7 @@ C4Context Rel(melodic_software_claude_code_plugins, melodic_software_provisioning, "cites (5)") Rel(melodic_software_claude_code_plugins, melodic_software_runner_policy_runtime, "cites (1)") Rel(melodic_software_claude_code_plugins, melodic_software_standards, "cites (53)") - Rel(melodic_software_claude_code_plugins, actions_checkout, "uses-workflow (15)") + Rel(melodic_software_claude_code_plugins, actions_checkout, "uses-workflow (11)") Rel(melodic_software_claude_code_plugins, melodic_software_ci_workflows, "uses-workflow (23)") ``` diff --git a/docs/ci-runner-routing.md b/docs/ci-runner-routing.md index dd39474113..926c78137a 100644 --- a/docs/ci-runner-routing.md +++ b/docs/ci-runner-routing.md @@ -1,9 +1,8 @@ # CI runner routing This repository is public, so every lane runs on GitHub-hosted runners, free for -public repositories: `ubuntu-24.04` for all of them except the two informational -Windows lanes, `test-windows` in `ci.yml` and `windows` in -`hook-utils-timing.yml`, which run `windows-2025`. The organization's +public repositories: `ubuntu-24.04` for all of them except the informational +Windows lane `test-windows` in `ci.yml`, which runs `windows-2025`. The organization's runner-policy engine refuses a governed fleet label here outright, reporting `public-self-hosted-routing`. There is no observer credential and no self-hosted exception inventory in this repository. diff --git a/lib/hook-utils.test.sh b/lib/hook-utils.test.sh index 1b656a8648..19194fea85 100755 --- a/lib/hook-utils.test.sh +++ b/lib/hook-utils.test.sh @@ -1822,252 +1822,6 @@ else fail "buffer_stdin late-EOF: rc=$bs_rc out=$(cat "$bs_out_file")" fi -# ...and it must cost ONE window, not two. Re-arming on progress would otherwise -# spend a second full window waiting for an EOF this producer never sends, -# doubling the delay the bound is supposed to cap; the loop therefore stops as -# soon as the buffer already parses as whole JSON. One window is the floor — -# until a window expires, a held-open pipe is indistinguishable from a slow one. -# -# There is no non-clock proxy to convert this to: both the correct and the -# reading-on implementations return rc 0 and the identical payload, so latency is -# the only observable that tells them apart. Everything below is about making a -# latency comparison that a loaded host cannot SYSTEMATICALLY invert. Not one it -# cannot invert at all: item 3 below is the correction of exactly that claim, and -# single deltas of -631 ms and -1012 ms have been measured here. What the -# machinery buys is that inversions stay isolated samples the estimator discards, -# instead of a standing offset that survives into the verdict. -# -# TWO PROPERTIES DO THE WORK, and a third that was claimed here does not. -# -# 1. The slow arm must do strictly MORE work than the fast one in every -# dimension. The override therefore performs the real completeness check and -# only LIES about the verdict. An override that SKIPS the work -# (`json_complete() { return 1; }`) makes the slow arm pay ZERO jq forks -# while the fast arm pays one, so on a host where a spawn costs seconds the -# "slow" arm wins: measured, 4855 ms fast vs 4330 ms slow, an inverted -# result. The ledger is fast = 1 read + 1 fork against slow = 5 reads + -# 3 forks (two json_complete probes plus the `validated=0` probe at -# hook-utils.sh:940, which only the slow arm reaches). -# -# 2. The producer must hold its stdout open until the CONSUMER is done. A fixed -# `sleep` cannot do that: reaching the slow arm's verdict costs the bound plus -# buffer_stdin's startup spawns, and when those spawns outran the hold, EOF cut -# the slow arm short and the comparison measured the hold instead of the -# behavior (the same run: slow arm 4330 ms against a 3 s hold). bs_hold_open -# replaces the sleep with a handshake, so the hold is exactly as long as the -# consumer needs and no constant has to be guessed. -# -# 3. NOT TRUE, and was asserted here: that more work on the slow side means "no -# amount of load can invert" the result. The two arms are separate processes -# run SEQUENTIALLY, so they never share a load sample — dominance holds in -# expectation, not per sample. Instrumenting buffer_stdin's two startup forks -# across four back-to-back runs on an idle box gave 93/92, 762/1277, -# 1755/3234, 107/100 ms: a 35x swing on ONE fork, up to 3.2 s. The structural -# gaps it has to be read against are 1.2 s for this case (one slice against a -# whole bound plus a slice) and 2.7 s for the stall case below, so a single -# bad fork exceeds one of them outright and eats most of the other. Since each -# arm pays several forks, single-sample comparison is not measurable here at -# all — which is why these cases take N interleaved samples per arm and -# compare an ORDER-BALANCED median of the paired deltas: a median inside each -# order group, averaged across the two groups (bs_paired_estimate). The median -# is robust to the occasional multi-second fork, which one sample is not, and -# the grouping is what keeps a systematic order bias from riding through it. -# -# Timing is taken INSIDE the consumer: bracketing the pipeline would fold the -# producer and the handshake into the measurement. - -# bs_median ... — the middle value. Used instead of a mean because the noise -# is a heavy tail (one 3.2 s fork in four samples), which a mean would swallow. -# At an even count this returns the LOWER middle rather than averaging the two; -# every caller here passes an odd-sized group, and where it does not the choice -# is conservative rather than pass-favoring. -bs_median() { printf '%s\n' "$@" | sort -n | awk '{v[NR] = $0} END {print v[int((NR + 1) / 2)]}'; } - -# bs_paired_estimate -- — prints -# " ", the order-balanced estimate of -# B-minus-A in ms. Returns 1 (printing nothing) if the separator is missing or -# the two groups are not the same size. -# -# Every delta inside ONE group carries the SAME order bias, because every pair in -# it ran in the same order: +δ where A ran first, since B then paid the -# second-position penalty, and -δ where B ran first. So a statistic taken -# symmetrically across the two groups cancels δ exactly, whatever δ is, while the -# same statistic POOLED over both groups does not — see bs_samples. -# -# The median WITHIN each group keeps the outlier rejection a pooled median had: -# a lone multi-second fork never moves its group's median. The mean ACROSS the -# two groups is what cancels the bias, and a mean is correct there precisely -# because its two inputs are equal-sized and oppositely biased. Equal size is the -# load-bearing precondition, so it is checked rather than assumed — an odd sample -# count would split 3/2 and cancel only part of the bias. -bs_paired_estimate() { - local -a a_first=() b_first=() - local seen_sep=0 arg m_a m_b - for arg in "$@"; do - if [[ "$arg" == "--" ]]; then - seen_sep=1 - continue - fi - if ((seen_sep)); then b_first+=("$arg"); else a_first+=("$arg"); fi - done - ((seen_sep == 1)) || return 1 - ((${#a_first[@]} > 0)) || return 1 - ((${#a_first[@]} == ${#b_first[@]})) || return 1 - m_a=$(bs_median "${a_first[@]}") - m_b=$(bs_median "${b_first[@]}") - printf '%s %s %s' "$(((m_a + m_b) / 2))" "$m_a" "$m_b" -} - -# bs_paired_verdict