From f3f80c1d9f26567e253d8745899ee8b886750650 Mon Sep 17 00:00:00 2001 From: Trey Kim Date: Mon, 12 Oct 2026 00:02:24 +0800 Subject: [PATCH 1/3] =?UTF-8?q?quality:=20Q0=20size=20gate=20=E2=80=94=20a?= =?UTF-8?q?rtifact=20inventory,=20strict=20schema,=20measurer,=20comparato?= =?UTF-8?q?r=20(RFC-0038a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit H = min(C, ceil(B*1.10)) computed in integers; CI passes iff actual <= H. Inventory maps RFC-0038a §4 artifacts to release.yml jobs; artifacts with no workflow path are marked 'unregistered' and listed, not failed (1.9b). --- .github/workflows/ci.yml | 4 + quality/artifacts.json | 1254 ++++++++++++++++++++++++++ quality/size-budgets.json | 4 + scripts/quality/budget_lib.py | 380 ++++++++ scripts/quality/check-budgets.py | 23 + scripts/quality/check-inventory.py | 24 + scripts/quality/measure-artifacts.py | 14 + scripts/quality/tests/test_q0.py | 231 +++++ 8 files changed, 1934 insertions(+) create mode 100644 quality/artifacts.json create mode 100644 quality/size-budgets.json create mode 100755 scripts/quality/budget_lib.py create mode 100755 scripts/quality/check-budgets.py create mode 100755 scripts/quality/check-inventory.py create mode 100755 scripts/quality/measure-artifacts.py create mode 100644 scripts/quality/tests/test_q0.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c1c7c748..f24cd27f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -404,6 +404,10 @@ jobs: run: python3 scripts/gen_providers_doc.py --check - name: Provider boundaries (no call-level reads or provider-name branches in providers, removed names stay removed) run: bash scripts/check_provider_boundaries.sh + - name: Q0 size gate (artifact inventory maps to workflow jobs; budget comparator negative tests) + run: | + python3 scripts/quality/check-inventory.py + python3 -m unittest discover -s scripts/quality/tests -v - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: ts-rs export drift (checked-in types must match generator output) diff --git a/quality/artifacts.json b/quality/artifacts.json new file mode 100644 index 000000000..79326e3b9 --- /dev/null +++ b/quality/artifacts.json @@ -0,0 +1,1254 @@ +{ + "schema_version": 1, + "artifacts": [ + { + "package": "aimux-ffi", + "kind": "staticlib", + "target": "x86_64-unknown-linux-gnu", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 70000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "go-build", + "matrix": { + "target": "x86_64-unknown-linux-gnu" + } + }, + "path": "libaimux_ffi-x86_64-unknown-linux-gnu.a" + }, + { + "package": "aimux-ffi", + "kind": "staticlib", + "target": "x86_64-apple-darwin", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 70000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "go-build", + "matrix": { + "target": "x86_64-apple-darwin" + } + }, + "path": "libaimux_ffi-x86_64-apple-darwin.a" + }, + { + "package": "aimux-ffi", + "kind": "staticlib", + "target": "aarch64-apple-darwin", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 70000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "go-build", + "matrix": { + "target": "aarch64-apple-darwin" + } + }, + "path": "libaimux_ffi-aarch64-apple-darwin.a" + }, + { + "package": "aimux-ffi", + "kind": "staticlib", + "target": "x86_64-pc-windows-gnu", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 70000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "go-build", + "matrix": { + "target": "x86_64-pc-windows-gnu" + } + }, + "path": "libaimux_ffi-x86_64-pc-windows-gnu.a" + }, + { + "package": "aimux-ffi", + "kind": "cdylib", + "target": "x86_64-unknown-linux-gnu", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 24200000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "ffi-build", + "matrix": { + "artifact": "libaimux_ffi-linux-x64.so" + } + }, + "path": "libaimux_ffi-linux-x64.so" + }, + { + "package": "aimux-ffi", + "kind": "cdylib", + "target": "aarch64-apple-darwin", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 24200000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "ffi-build", + "matrix": { + "artifact": "libaimux_ffi-macos-arm64.dylib" + } + }, + "path": "libaimux_ffi-macos-arm64.dylib" + }, + { + "package": "aimux-ffi", + "kind": "cdylib", + "target": "x86_64-pc-windows-msvc", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 24200000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "ffi-build", + "matrix": { + "artifact": "aimux_ffi-windows-x64.dll" + } + }, + "path": "aimux_ffi-windows-x64.dll" + }, + { + "package": "aimux-ffi", + "kind": "ios-framework-binary", + "target": "aarch64-apple-ios", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 67108864 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "flutter-ffi-mobile", + "matrix": { + "targets": "aarch64-apple-ios" + } + } + }, + { + "package": "aimux-ffi", + "kind": "ios-framework-binary", + "target": "aarch64-apple-ios-sim", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 67108864 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "flutter-ffi-mobile", + "matrix": { + "targets": "aarch64-apple-ios-sim" + } + } + }, + { + "package": "aimux-ffi", + "kind": "ios-xcframework", + "target": "aarch64-apple-ios+aarch64-apple-ios-sim", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "unpacked": { + "product_cap_bytes": 136314880 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "flutter-ffi-mobile", + "matrix": { + "platform": "ios" + } + }, + "path": "aimux_ffi.xcframework" + }, + { + "package": "aimux-ffi", + "kind": "ios-xcframework-info-plist", + "target": "aarch64-apple-ios+aarch64-apple-ios-sim", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 1000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "flutter-ffi-mobile", + "matrix": { + "platform": "ios" + } + }, + "path": "aimux_ffi.xcframework/Info.plist" + }, + { + "package": "aimux-ffi", + "kind": "android-so", + "target": "aarch64-linux-android", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 9000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "flutter-ffi-mobile", + "matrix": { + "targets": "aarch64-linux-android" + } + }, + "path": "android/arm64-v8a/libaimux_ffi.so" + }, + { + "package": "aimux-ffi", + "kind": "android-so", + "target": "armv7-linux-androideabi", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 9000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "flutter-ffi-mobile", + "matrix": { + "targets": "armv7-linux-androideabi" + } + }, + "path": "android/armeabi-v7a/libaimux_ffi.so" + }, + { + "package": "aimux-ffi", + "kind": "android-so", + "target": "x86_64-linux-android", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 9000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "flutter-ffi-mobile", + "matrix": { + "targets": "x86_64-linux-android" + } + }, + "path": "android/x86_64/libaimux_ffi.so" + }, + { + "package": "aimux-ffi", + "kind": "android-so-total", + "target": "arm64-v8a+armeabi-v7a+x86_64", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "unpacked": { + "product_cap_bytes": 23100000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "flutter-ffi-mobile", + "matrix": { + "platform": "android" + } + }, + "path": "android" + }, + { + "package": "aimux-cli", + "kind": "bin", + "target": "x86_64-unknown-linux-gnu", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 32000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "console-build", + "matrix": { + "target": "x86_64-unknown-linux-gnu" + } + }, + "path": "aimux-cli-linux-x64" + }, + { + "package": "aimux-cli", + "kind": "bin", + "target": "aarch64-apple-darwin", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 32000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "console-build", + "matrix": { + "target": "aarch64-apple-darwin" + } + }, + "path": "aimux-cli-macos-arm64" + }, + { + "package": "aimux-cli", + "kind": "bin", + "target": "x86_64-pc-windows-msvc", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 32000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "console-build", + "matrix": { + "target": "x86_64-pc-windows-msvc" + } + }, + "path": "aimux-cli-windows-x64.exe" + }, + { + "package": "aimux-replay", + "kind": "bin", + "target": "x86_64-unknown-linux-gnu", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 32000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "console-build", + "matrix": { + "target": "x86_64-unknown-linux-gnu" + } + }, + "path": "aimux-replay-linux-x64" + }, + { + "package": "aimux-replay", + "kind": "bin", + "target": "aarch64-apple-darwin", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 32000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "console-build", + "matrix": { + "target": "aarch64-apple-darwin" + } + }, + "path": "aimux-replay-macos-arm64" + }, + { + "package": "aimux-replay", + "kind": "bin", + "target": "x86_64-pc-windows-msvc", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 32000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "console-build", + "matrix": { + "target": "x86_64-pc-windows-msvc" + } + }, + "path": "aimux-replay-windows-x64.exe" + }, + { + "package": "aimux-web", + "kind": "bin", + "target": "x86_64-unknown-linux-gnu", + "feature_set": "embed-frontend", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 40000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "console-build", + "matrix": { + "target": "x86_64-unknown-linux-gnu" + } + }, + "path": "aimux-web-linux-x64" + }, + { + "package": "aimux-web", + "kind": "bin", + "target": "aarch64-apple-darwin", + "feature_set": "embed-frontend", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 40000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "console-build", + "matrix": { + "target": "aarch64-apple-darwin" + } + }, + "path": "aimux-web-macos-arm64" + }, + { + "package": "aimux-web", + "kind": "bin", + "target": "x86_64-pc-windows-msvc", + "feature_set": "embed-frontend", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 40000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "console-build", + "matrix": { + "target": "x86_64-pc-windows-msvc" + } + }, + "path": "aimux-web-windows-x64.exe" + }, + { + "package": "aimux-node", + "kind": "node-addon", + "target": "win32-x64-msvc", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 24200000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "node-build", + "matrix": { + "target": "x86_64-pc-windows-msvc" + } + }, + "path": "aimux.win32-x64-msvc.node", + "note": "npm package @arcships/aimux" + }, + { + "package": "aimux-node-win32-x64-msvc", + "kind": "npm-tgz", + "target": "win32-x64-msvc", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 24500000 + }, + "unpacked": { + "product_cap_bytes": 25000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "node-publish" + }, + "note": "npm package @arcships/aimux-win32-x64-msvc; published by napi prepublish (bindings/node/package.json prepublishOnly)" + }, + { + "package": "aimux-node", + "kind": "node-addon", + "target": "win32-arm64-msvc", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 24200000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "node-build", + "matrix": { + "target": "aarch64-pc-windows-msvc" + } + }, + "path": "aimux.win32-arm64-msvc.node", + "note": "npm package @arcships/aimux" + }, + { + "package": "aimux-node-win32-arm64-msvc", + "kind": "npm-tgz", + "target": "win32-arm64-msvc", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 24500000 + }, + "unpacked": { + "product_cap_bytes": 25000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "node-publish" + }, + "note": "npm package @arcships/aimux-win32-arm64-msvc; published by napi prepublish (bindings/node/package.json prepublishOnly)" + }, + { + "package": "aimux-node", + "kind": "node-addon", + "target": "darwin-x64", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 24200000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "node-build", + "matrix": { + "target": "x86_64-apple-darwin" + } + }, + "path": "aimux.darwin-x64.node", + "note": "npm package @arcships/aimux" + }, + { + "package": "aimux-node-darwin-x64", + "kind": "npm-tgz", + "target": "darwin-x64", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 24500000 + }, + "unpacked": { + "product_cap_bytes": 25000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "node-publish" + }, + "note": "npm package @arcships/aimux-darwin-x64; published by napi prepublish (bindings/node/package.json prepublishOnly)" + }, + { + "package": "aimux-node", + "kind": "node-addon", + "target": "darwin-arm64", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 24200000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "node-build", + "matrix": { + "target": "aarch64-apple-darwin" + } + }, + "path": "aimux.darwin-arm64.node", + "note": "npm package @arcships/aimux" + }, + { + "package": "aimux-node-darwin-arm64", + "kind": "npm-tgz", + "target": "darwin-arm64", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 24500000 + }, + "unpacked": { + "product_cap_bytes": 25000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "node-publish" + }, + "note": "npm package @arcships/aimux-darwin-arm64; published by napi prepublish (bindings/node/package.json prepublishOnly)" + }, + { + "package": "aimux-node", + "kind": "node-addon", + "target": "linux-x64-gnu", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 24200000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "node-build", + "matrix": { + "target": "x86_64-unknown-linux-gnu" + } + }, + "path": "aimux.linux-x64-gnu.node", + "note": "npm package @arcships/aimux" + }, + { + "package": "aimux-node-linux-x64-gnu", + "kind": "npm-tgz", + "target": "linux-x64-gnu", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 24500000 + }, + "unpacked": { + "product_cap_bytes": 25000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "node-publish" + }, + "note": "npm package @arcships/aimux-linux-x64-gnu; published by napi prepublish (bindings/node/package.json prepublishOnly)" + }, + { + "package": "aimux-node", + "kind": "node-addon", + "target": "linux-arm64-gnu", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 24200000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "node-build", + "matrix": { + "target": "aarch64-unknown-linux-gnu" + } + }, + "path": "aimux.linux-arm64-gnu.node", + "note": "npm package @arcships/aimux" + }, + { + "package": "aimux-node-linux-arm64-gnu", + "kind": "npm-tgz", + "target": "linux-arm64-gnu", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 24500000 + }, + "unpacked": { + "product_cap_bytes": 25000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "node-publish" + }, + "note": "npm package @arcships/aimux-linux-arm64-gnu; published by napi prepublish (bindings/node/package.json prepublishOnly)" + }, + { + "package": "aimux-node", + "kind": "npm-tgz", + "target": "any", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 2000000 + }, + "unpacked": { + "product_cap_bytes": 5000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "node-publish" + }, + "note": "npm package @arcships/aimux" + }, + { + "package": "aimux-python", + "kind": "wheel", + "target": "linux-x64", + "feature_set": "abi3-py38", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 24200000 + }, + "packed": { + "product_cap_bytes": 24500000 + }, + "unpacked": { + "product_cap_bytes": 25000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "python-release", + "matrix": { + "os": "ubuntu-latest" + } + }, + "note": "actual platform tag must be read from wheel metadata at measure time (RFC-0038a §4.2)" + }, + { + "package": "aimux-python", + "kind": "wheel", + "target": "macos-arm64", + "feature_set": "abi3-py38", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 24200000 + }, + "packed": { + "product_cap_bytes": 24500000 + }, + "unpacked": { + "product_cap_bytes": 25000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "python-release", + "matrix": { + "os": "macos-latest" + } + }, + "note": "actual platform tag must be read from wheel metadata at measure time (RFC-0038a §4.2)" + }, + { + "package": "aimux-python", + "kind": "wheel", + "target": "windows-x64", + "feature_set": "abi3-py38", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 24200000 + }, + "packed": { + "product_cap_bytes": 24500000 + }, + "unpacked": { + "product_cap_bytes": 25000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "python-release", + "matrix": { + "os": "windows-latest" + } + }, + "note": "actual platform tag must be read from wheel metadata at measure time (RFC-0038a §4.2)" + }, + { + "package": "aimux-stream", + "kind": "crate", + "target": "any", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 9500000 + }, + "unpacked": { + "product_cap_bytes": 20000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "rust-publish" + } + }, + { + "package": "aimux-core", + "kind": "crate", + "target": "any", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 9500000 + }, + "unpacked": { + "product_cap_bytes": 20000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "rust-publish" + } + }, + { + "package": "aimux-provider-utils", + "kind": "crate", + "target": "any", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 9500000 + }, + "unpacked": { + "product_cap_bytes": 20000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "rust-publish" + } + }, + { + "package": "aimux-providers", + "kind": "crate", + "target": "any", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 9500000 + }, + "unpacked": { + "product_cap_bytes": 20000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "rust-publish" + } + }, + { + "package": "aimux-ffi", + "kind": "crate", + "target": "any", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 9500000 + }, + "unpacked": { + "product_cap_bytes": 20000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "rust-publish" + } + }, + { + "package": "aimux-java", + "kind": "jar-main", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 2000000 + }, + "unpacked": { + "product_cap_bytes": 5000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "jvm-publish" + } + }, + { + "package": "aimux-java", + "kind": "jar-sources", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 2000000 + }, + "unpacked": { + "product_cap_bytes": 5000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "jvm-publish" + } + }, + { + "package": "aimux-java", + "kind": "jar-javadoc", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 10000000 + }, + "unpacked": { + "product_cap_bytes": 30000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "jvm-publish" + } + }, + { + "package": "aimux-java", + "kind": "maven-pom", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 1000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "jvm-publish" + } + }, + { + "package": "aimux-java", + "kind": "gradle-module-metadata", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 1000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "jvm-publish" + } + }, + { + "package": "aimux-java", + "kind": "pgp-signature", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 16384 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "jvm-publish" + }, + "note": "signing.sign(mavenJava) with SIGNING_KEY secret" + }, + { + "package": "aimux-java", + "kind": "checksum", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 1024 + } + }, + "status": "unregistered", + "reason": "md5/sha1 files are generated implicitly by Gradle maven-publish during upload; no workflow step produces a stageable file to measure" + }, + { + "package": "aimux-kotlin", + "kind": "jar-main", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 2000000 + }, + "unpacked": { + "product_cap_bytes": 5000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "jvm-publish" + } + }, + { + "package": "aimux-kotlin", + "kind": "jar-sources", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 2000000 + }, + "unpacked": { + "product_cap_bytes": 5000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "jvm-publish" + } + }, + { + "package": "aimux-kotlin", + "kind": "jar-javadoc", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 10000000 + }, + "unpacked": { + "product_cap_bytes": 30000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "jvm-publish" + } + }, + { + "package": "aimux-kotlin", + "kind": "maven-pom", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 1000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "jvm-publish" + } + }, + { + "package": "aimux-kotlin", + "kind": "gradle-module-metadata", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 1000000 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "jvm-publish" + } + }, + { + "package": "aimux-kotlin", + "kind": "pgp-signature", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 16384 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "jvm-publish" + }, + "note": "signing.sign(mavenJava) with SIGNING_KEY secret" + }, + { + "package": "aimux-kotlin", + "kind": "checksum", + "target": "jvm", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 1024 + } + }, + "status": "unregistered", + "reason": "md5/sha1 files are generated implicitly by Gradle maven-publish during upload; no workflow step produces a stageable file to measure" + }, + { + "package": "aimux-ffi", + "kind": "c-header", + "target": "any", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "raw": { + "product_cap_bytes": 1000000 + } + }, + "status": "unregistered", + "reason": "aimux-ffi/aimux-ffi.h is only uploaded by ci.yml ffi-build (a CI artifact); no release.yml job publishes it, so there is no release path to register" + }, + { + "package": "aimux-flutter", + "kind": "pub-archive", + "target": "any", + "feature_set": "default", + "build_variant": "release", + "metrics": { + "packed": { + "product_cap_bytes": 34870000 + }, + "unpacked": { + "product_cap_bytes": 134217728 + } + }, + "status": "mapped", + "workflow": { + "file": ".github/workflows/release.yml", + "job": "flutter-publish" + }, + "note": "flutter pub publish builds the archive itself; measuring the exact upload needs a frozen-directory step (RFC-0038a §4.3)" + } + ] +} diff --git a/quality/size-budgets.json b/quality/size-budgets.json new file mode 100644 index 000000000..ac77ec457 --- /dev/null +++ b/quality/size-budgets.json @@ -0,0 +1,4 @@ +{ + "schema_version": 1, + "budgets": [] +} diff --git a/scripts/quality/budget_lib.py b/scripts/quality/budget_lib.py new file mode 100755 index 000000000..d3d1162be --- /dev/null +++ b/scripts/quality/budget_lib.py @@ -0,0 +1,380 @@ +"""Q0 size gate (RFC-0038a §3, §9.1): inventory, measurer and comparator. + +Pure standard library; all byte values are integers. No floating point is +used when deriving H, so ceil(B * 1.10) is exact. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import tarfile +import zipfile +from pathlib import Path + +SCHEMA_VERSION = 1 +METRICS = ("raw", "packed", "unpacked") +STATUS_MAPPED = "mapped" +STATUS_UNREGISTERED = "unregistered" + +# Failure categories (RFC-0038a §3.2, §9.4). +BUDGET_EXCEEDED = "BUDGET_EXCEEDED" +BUDGET_UNREGISTERED = "BUDGET_UNREGISTERED" +ARTIFACT_MISSING = "ARTIFACT_MISSING" +BUDGET_INVALID = "BUDGET_INVALID" + + +class SchemaError(ValueError): + pass + + +# ── integer helpers ────────────────────────────────────────────────────────── + + +def _require_bytes(value, where: str) -> int: + # bool is an int subclass; reject it along with floats and negatives. + if isinstance(value, bool) or not isinstance(value, int): + raise SchemaError(f"{where}: byte count must be an integer, got {value!r}") + if value < 0: + raise SchemaError(f"{where}: byte count must be non-negative, got {value}") + return value + + +def hard_cap(baseline: int, product_cap: int) -> int: + """H = min(C, ceil(B × 1.10)) — RFC-0038a §3.2, integer-exact.""" + _require_bytes(baseline, "B") + _require_bytes(product_cap, "C") + return min(product_cap, (baseline * 11 + 9) // 10) + + +def passes(actual: int, cap: int) -> bool: + """Normal CI compares only actual <= H (RFC-0038a §3.2).""" + return _require_bytes(actual, "actual") <= _require_bytes(cap, "H") + + +# ── strict JSON loading ────────────────────────────────────────────────────── + + +def _no_dup_keys(pairs): + out = {} + for k, v in pairs: + if k in out: + raise SchemaError(f"duplicate JSON key {k!r}") + out[k] = v + return out + + +def _reject_float(text): + raise SchemaError(f"floating point number {text} is not allowed") + + +def load_json(path) -> dict: + with open(path, encoding="utf-8") as fh: + return json.load( + fh, object_pairs_hook=_no_dup_keys, parse_float=_reject_float + ) + + +def _check_keys(obj, required, optional, where): + if not isinstance(obj, dict): + raise SchemaError(f"{where}: expected an object") + missing = [k for k in required if k not in obj] + if missing: + raise SchemaError(f"{where}: missing field(s) {missing}") + unknown = sorted(set(obj) - set(required) - set(optional)) + if unknown: + raise SchemaError(f"{where}: unknown field(s) {unknown}") + + +def _check_schema_version(doc, where): + if doc.get("schema_version") != SCHEMA_VERSION: + raise SchemaError(f"{where}: schema_version must be {SCHEMA_VERSION}") + + +ID_PARTS = ("package", "kind", "target", "feature_set", "build_variant") + + +def artifact_id(entry: dict) -> str: + return "/".join(entry[p] for p in ID_PARTS) + + +def validate_manifest(doc: dict) -> dict: + _check_keys(doc, ["schema_version", "artifacts"], [], "manifest") + _check_schema_version(doc, "manifest") + seen = set() + for i, a in enumerate(doc["artifacts"]): + where = f"artifacts[{i}]" + _check_keys( + a, + [*ID_PARTS, "metrics", "status"], + ["path", "workflow", "reason", "note"], + where, + ) + for p in ID_PARTS: + if not isinstance(a[p], str) or not a[p] or "/" in a[p]: + raise SchemaError(f"{where}.{p}: must be a non-empty string without '/'") + if "*" in a[p]: + raise SchemaError(f"{where}.{p}: unconstrained glob is not allowed") + aid = artifact_id(a) + if aid in seen: + raise SchemaError(f"{where}: duplicate artifact id {aid}") + seen.add(aid) + m = a["metrics"] + if not isinstance(m, dict) or not m: + raise SchemaError(f"{where}.metrics: must be a non-empty object") + for metric, spec in m.items(): + if metric not in METRICS: + raise SchemaError(f"{where}.metrics: unknown metric {metric!r}") + _check_keys(spec, ["product_cap_bytes"], [], f"{where}.metrics.{metric}") + _require_bytes(spec["product_cap_bytes"], f"{aid}:{metric}:C") + if a["status"] == STATUS_MAPPED: + wf = a.get("workflow") + _check_keys(wf, ["file", "job"], ["matrix"], f"{where}.workflow") + if "reason" in a: + raise SchemaError(f"{where}: mapped artifact must not carry 'reason'") + elif a["status"] == STATUS_UNREGISTERED: + if "workflow" in a: + raise SchemaError(f"{where}: unregistered artifact must not carry 'workflow'") + if not a.get("reason"): + raise SchemaError(f"{where}: unregistered artifact needs a 'reason'") + else: + raise SchemaError(f"{where}.status: must be mapped or unregistered") + if "path" in a and "*" in a["path"]: + raise SchemaError(f"{where}.path: unconstrained glob is not allowed") + return doc + + +def validate_budgets(doc: dict, manifest: dict) -> dict: + _check_keys(doc, ["schema_version", "budgets"], [], "budgets") + _check_schema_version(doc, "budgets") + caps = { + (artifact_id(a), metric): spec["product_cap_bytes"] + for a in manifest["artifacts"] + for metric, spec in a["metrics"].items() + } + seen = set() + for i, b in enumerate(doc["budgets"]): + where = f"budgets[{i}]" + _check_keys( + b, + ["artifact_id", "metric", "baseline_bytes", "hard_cap_bytes", "product_cap_bytes", "evidence"], + ["approved_at", "source_sha"], + where, + ) + key = (b["artifact_id"], b["metric"]) + if key in seen: + raise SchemaError(f"{where}: duplicate budget {key}") + seen.add(key) + if key not in caps: + raise SchemaError(f"{where}: {key} is not in the artifact manifest") + B = _require_bytes(b["baseline_bytes"], f"{where}.B") + H = _require_bytes(b["hard_cap_bytes"], f"{where}.H") + C = _require_bytes(b["product_cap_bytes"], f"{where}.C") + if C != caps[key]: + raise SchemaError(f"{where}: C={C} disagrees with manifest C={caps[key]}") + if B > C: + raise SchemaError(f"{where}: B={B} > C={C}; cannot register as green") + if H != hard_cap(B, C): + raise SchemaError( + f"{where}: stored H={H} != min(C, ceil(B*1.10))={hard_cap(B, C)}" + ) + return doc + + +# ── workflow inventory mapping ─────────────────────────────────────────────── + +_JOB_RE = re.compile(r"^ ([A-Za-z0-9_-]+):\s*$") + + +def workflow_jobs(text: str) -> dict: + """Map top-level job id -> (1-based line, job block text). + + Minimal parser for the repository's own workflow layout (two-space job + keys under `jobs:`); avoids a PyYAML dependency. + """ + lines = text.splitlines() + try: + start = next(i for i, l in enumerate(lines) if l.rstrip() == "jobs:") + except StopIteration: + return {} + jobs, cur, buf = {}, None, [] + for i in range(start + 1, len(lines)): + line = lines[i] + m = _JOB_RE.match(line) + if m: + if cur: + jobs[cur[0]] = (cur[1], "\n".join(buf)) + cur, buf = (m.group(1), i + 1), [] + elif line and not line.startswith(" ") and not line.startswith("#"): + break + else: + buf.append(line) + if cur: + jobs[cur[0]] = (cur[1], "\n".join(buf)) + return jobs + + +def check_inventory(manifest: dict, repo_root) -> list: + """Return a list of problems with workflow mappings (empty = OK). + + Unregistered artifacts are reported, never failed (PHASES 1.9 correction). + """ + root = Path(repo_root) + cache, problems = {}, [] + for a in manifest["artifacts"]: + if a["status"] != STATUS_MAPPED: + continue + wf = a["workflow"] + if wf["file"] not in cache: + p = root / wf["file"] + cache[wf["file"]] = workflow_jobs(p.read_text()) if p.exists() else None + jobs = cache[wf["file"]] + aid = artifact_id(a) + if jobs is None: + problems.append(f"{aid}: workflow file {wf['file']} not found") + continue + if wf["job"] not in jobs: + problems.append(f"{aid}: job {wf['job']!r} not in {wf['file']}") + continue + body = jobs[wf["job"]][1] + for k, v in (wf.get("matrix") or {}).items(): + if not re.search(rf"\b{re.escape(k)}:\s*\[?.*\b{re.escape(v)}\b", body): + problems.append(f"{aid}: matrix {k}={v} not found in job {wf['job']!r}") + return problems + + +def mapping_rows(manifest: dict, repo_root): + """(artifact_id, status, file:line or reason) for every artifact.""" + root = Path(repo_root) + cache, rows = {}, [] + for a in manifest["artifacts"]: + aid = artifact_id(a) + if a["status"] == STATUS_UNREGISTERED: + rows.append((aid, STATUS_UNREGISTERED, a["reason"])) + continue + wf = a["workflow"] + if wf["file"] not in cache: + cache[wf["file"]] = workflow_jobs((root / wf["file"]).read_text()) + line = cache[wf["file"]][wf["job"]][0] + mx = ",".join(f"{k}={v}" for k, v in (wf.get("matrix") or {}).items()) + rows.append((aid, STATUS_MAPPED, f"{wf['file']}:{line} {wf['job']}" + (f" [{mx}]" if mx else ""))) + return rows + + +# ── measurer ───────────────────────────────────────────────────────────────── + + +def _unpacked_bytes(path: Path) -> int: + name = path.name + total, seen = 0, set() + + def member(mname, size, is_file, is_dir): + nonlocal total + norm = os.path.normpath(mname) + if mname.startswith("/") or norm == ".." or norm.startswith("../"): + raise SchemaError(f"{path}: path traversal member {mname!r}") + if is_dir: + return + if not is_file: + raise SchemaError(f"{path}: unsupported member type {mname!r}") + if norm in seen: + raise SchemaError(f"{path}: duplicate member path {mname!r}") + seen.add(norm) + total += size + + if name.endswith((".tgz", ".tar.gz", ".tar")): + with tarfile.open(path) as tf: + for m in tf.getmembers(): + member(m.name, m.size, m.isfile(), m.isdir()) + elif name.endswith((".whl", ".jar", ".zip")): + with zipfile.ZipFile(path) as zf: + for info in zf.infolist(): + is_link = (info.external_attr >> 16) & 0o170000 == 0o120000 + member(info.filename, info.file_size, not info.is_dir() and not is_link, info.is_dir()) + elif path.is_dir(): + for p in sorted(path.rglob("*")): + rel = p.relative_to(path).as_posix() + if p.is_symlink(): + raise SchemaError(f"{path}: symlink member {rel!r}") + member(rel, p.stat().st_size if p.is_file() else 0, p.is_file(), p.is_dir()) + else: + raise SchemaError(f"{path}: cannot compute unpacked bytes for this format") + return total + + +def _sha256(path: Path) -> str: + h = hashlib.sha256() + with open(path, "rb") as fh: + for chunk in iter(lambda: fh.read(1 << 20), b""): + h.update(chunk) + return h.hexdigest() + + +def measure(manifest: dict, staging) -> dict: + """Measure every mapped artifact with a `path` present under staging.""" + staging = Path(staging) + out = [] + for a in manifest["artifacts"]: + if a["status"] != STATUS_MAPPED or "path" not in a: + continue + p = staging / a["path"] + if not p.exists(): + continue + rec = {"artifact_id": artifact_id(a), "file": a["path"], "metrics": {}} + if p.is_file(): + rec["sha256"] = _sha256(p) + for metric in a["metrics"]: + if metric in ("raw", "packed"): + if p.is_dir(): + raise SchemaError(f"{p}: {metric} needs a file") + rec["metrics"][metric] = p.stat().st_size + else: + rec["metrics"][metric] = _unpacked_bytes(p) + out.append(rec) + return {"schema_version": SCHEMA_VERSION, "measurements": out} + + +# ── comparator ─────────────────────────────────────────────────────────────── + + +def compare(manifest: dict, budgets: dict, report: dict) -> dict: + """Return {'failures': [...], 'passed': [...], 'unregistered': [...]}. + + - mapped artifact, budget present, measured: pass iff actual <= H + - mapped artifact, budget present, not measured: ARTIFACT_MISSING + - mapped artifact, no budget: BUDGET_UNREGISTERED + - inventory status 'unregistered': listed, never a failure (1.9b) + """ + measured = {} + for r in report["measurements"]: + for metric, v in r["metrics"].items(): + measured[(r["artifact_id"], metric)] = _require_bytes(v, r["artifact_id"]) + bud = {(b["artifact_id"], b["metric"]): b for b in budgets["budgets"]} + failures, passed, unregistered = [], [], [] + for a in manifest["artifacts"]: + aid = artifact_id(a) + if a["status"] == STATUS_UNREGISTERED: + unregistered.append({"artifact_id": aid, "reason": a["reason"]}) + continue + for metric in a["metrics"]: + key = (aid, metric) + b = bud.get(key) + if b is None: + failures.append({"artifact_id": aid, "metric": metric, "category": BUDGET_UNREGISTERED}) + continue + H = b["hard_cap_bytes"] + if H != hard_cap(b["baseline_bytes"], b["product_cap_bytes"]): + failures.append({"artifact_id": aid, "metric": metric, "category": BUDGET_INVALID}) + continue + if key not in measured: + failures.append({"artifact_id": aid, "metric": metric, "category": ARTIFACT_MISSING, "limit": H}) + continue + actual = measured[key] + rec = {"artifact_id": aid, "metric": metric, "actual": actual, "limit": H, "delta": actual - H} + if passes(actual, H): + passed.append(rec) + else: + failures.append({**rec, "category": BUDGET_EXCEEDED}) + return {"failures": failures, "passed": passed, "unregistered": unregistered} diff --git a/scripts/quality/check-budgets.py b/scripts/quality/check-budgets.py new file mode 100755 index 000000000..53dcedb79 --- /dev/null +++ b/scripts/quality/check-budgets.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Compare a size report against registered budgets (RFC-0038a §3.2, §9.1). + +Passes iff every registered metric has actual <= H. Inventory entries marked +'unregistered' are listed and never fail (PHASES 1.9; completion is 1.9b). +--report-only is the Q0 transitional mode (RFC-0038a §12): print, exit 0. +""" +import argparse, json, sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import budget_lib as bl + +ap = argparse.ArgumentParser() +ap.add_argument("--manifest", default="quality/artifacts.json") +ap.add_argument("--budgets", default="quality/size-budgets.json") +ap.add_argument("--report", required=True) +ap.add_argument("--report-only", action="store_true") +a = ap.parse_args() +m = bl.validate_manifest(bl.load_json(a.manifest)) +b = bl.validate_budgets(bl.load_json(a.budgets), m) +res = bl.compare(m, b, bl.load_json(a.report)) +print(json.dumps(res, indent=2)) +sys.exit(0 if a.report_only or not res["failures"] else 1) diff --git a/scripts/quality/check-inventory.py b/scripts/quality/check-inventory.py new file mode 100755 index 000000000..7501b11d9 --- /dev/null +++ b/scripts/quality/check-inventory.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Validate quality/artifacts.json and its mapping to .github/workflows jobs. + +Mapped artifacts must name an existing job (and matrix value); artifacts +marked 'unregistered' are printed but do not fail (PHASES 1.9 / 1.9b). +""" +import argparse, sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import budget_lib as bl + +ap = argparse.ArgumentParser() +ap.add_argument("--manifest", default="quality/artifacts.json") +ap.add_argument("--budgets", default="quality/size-budgets.json") +ap.add_argument("--root", default=".") +a = ap.parse_args() +m = bl.validate_manifest(bl.load_json(a.manifest)) +bl.validate_budgets(bl.load_json(a.budgets), m) +for aid, status, where in bl.mapping_rows(m, a.root) if not bl.check_inventory(m, a.root) else []: + print(f"{status:12} {aid} {where}") +problems = bl.check_inventory(m, a.root) +for p in problems: + print("ERROR", p, file=sys.stderr) +sys.exit(1 if problems else 0) diff --git a/scripts/quality/measure-artifacts.py b/scripts/quality/measure-artifacts.py new file mode 100755 index 000000000..ee176f054 --- /dev/null +++ b/scripts/quality/measure-artifacts.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +"""Measure staged release artifacts (RFC-0038a §9.1).""" +import argparse, json, sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import budget_lib as bl + +ap = argparse.ArgumentParser() +ap.add_argument("--manifest", required=True) +ap.add_argument("--input", required=True) +ap.add_argument("--out", required=True) +a = ap.parse_args() +m = bl.validate_manifest(bl.load_json(a.manifest)) +Path(a.out).write_text(json.dumps(bl.measure(m, a.input), indent=2) + "\n") diff --git a/scripts/quality/tests/test_q0.py b/scripts/quality/tests/test_q0.py new file mode 100644 index 000000000..15724aa16 --- /dev/null +++ b/scripts/quality/tests/test_q0.py @@ -0,0 +1,231 @@ +"""Q0 negative and schema tests (RFC-0038a §3.2, §9.1; PHASES 1.9).""" + +import io +import json +import sys +import tarfile +import tempfile +import unittest +import zipfile +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE.parent)) +import budget_lib as bl # noqa: E402 + +REPO = HERE.parents[2] + + +def _manifest(cap, status="mapped", metric="raw"): + a = { + "package": "pkg", "kind": "bin", "target": "t", "feature_set": "default", + "build_variant": "release", "metrics": {metric: {"product_cap_bytes": cap}}, + "status": status, "path": "f.bin", + } + if status == "mapped": + a["workflow"] = {"file": ".github/workflows/release.yml", "job": "x"} + else: + a["reason"] = "not built by any workflow" + return {"schema_version": 1, "artifacts": [a]} + + +def _budgets(B, C, H=None, metric="raw"): + return {"schema_version": 1, "budgets": [{ + "artifact_id": "pkg/bin/t/default/release", "metric": metric, + "baseline_bytes": B, "hard_cap_bytes": bl.hard_cap(B, C) if H is None else H, + "product_cap_bytes": C, "evidence": "test", + }]} + + +def _report(actual, metric="raw"): + return {"schema_version": 1, "measurements": [ + {"artifact_id": "pkg/bin/t/default/release", "file": "f.bin", "metrics": {metric: actual}} + ]} + + +def _run(B, C, actual): + m = _manifest(C) + b = bl.validate_budgets(_budgets(B, C), m) + return bl.compare(m, b, _report(actual)) + + +class HardCapFormula(unittest.TestCase): + def test_ceil_is_integer_exact(self): + self.assertEqual(bl.hard_cap(1000, 10**9), 1100) + self.assertEqual(bl.hard_cap(1001, 10**9), 1102) # 1101.1 -> 1102 + self.assertEqual(bl.hard_cap(1009, 10**9), 1110) # 1109.9 -> 1110 + self.assertEqual(bl.hard_cap(0, 10), 0) + # Large values where float 1.10 multiplication would round wrongly. + B = 66_500_001 + self.assertEqual(bl.hard_cap(B, 10**12), -(-B * 11 // 10)) + + def test_product_cap_wins_when_smaller(self): + self.assertEqual(bl.hard_cap(1000, 1050), 1050) + + +class NegativeGate(unittest.TestCase): + """PHASES 1.9 correction: actual == H passes, H + 1 is blocked.""" + + def test_actual_equal_H_passes(self): + B, C = 1000, 10**9 + H = bl.hard_cap(B, C) + res = _run(B, C, H) + self.assertEqual(res["failures"], []) + self.assertEqual(res["passed"][0]["actual"], H) + + def test_actual_H_plus_one_blocked(self): + B, C = 1000, 10**9 + H = bl.hard_cap(B, C) + res = _run(B, C, H + 1) + self.assertEqual(len(res["failures"]), 1) + f = res["failures"][0] + self.assertEqual(f["category"], bl.BUDGET_EXCEEDED) + self.assertEqual((f["actual"], f["limit"], f["delta"]), (H + 1, H, 1)) + + def test_rounded_ceiling_boundary(self): + # B=1001: ceil(1101.1)=1102 passes; 1103 blocked. + self.assertEqual(_run(1001, 10**9, 1102)["failures"], []) + self.assertEqual(_run(1001, 10**9, 1103)["failures"][0]["category"], bl.BUDGET_EXCEEDED) + + def test_C_below_ceil_B_times_1_10_blocks(self): + # C=1050 < ceil(1000*1.10)=1100 -> H=1050; 1051..1100 are blocked + # even though they are within +10% of B. + B, C = 1000, 1050 + self.assertEqual(bl.hard_cap(B, C), C) + self.assertEqual(_run(B, C, 1050)["failures"], []) + for actual in (1051, 1100): + f = _run(B, C, actual)["failures"] + self.assertEqual(f[0]["category"], bl.BUDGET_EXCEEDED, actual) + self.assertEqual(f[0]["limit"], 1050) + + def test_stored_H_not_recomputed_from_candidate(self): + # Comparator uses stored H; a tampered H is rejected at load. + with self.assertRaises(bl.SchemaError): + bl.validate_budgets(_budgets(1000, 10**9, H=1200), _manifest(10**9)) + + def test_B_greater_than_C_cannot_register(self): + with self.assertRaises(bl.SchemaError): + bl.validate_budgets(_budgets(2000, 1000, H=1000), _manifest(1000)) + + def test_missing_budget_is_unregistered_failure(self): + m = _manifest(10) + res = bl.compare(m, {"schema_version": 1, "budgets": []}, _report(1)) + self.assertEqual(res["failures"][0]["category"], bl.BUDGET_UNREGISTERED) + + def test_missing_measurement_is_artifact_missing(self): + m = _manifest(10**9) + b = _budgets(1000, 10**9) + res = bl.compare(m, b, {"schema_version": 1, "measurements": []}) + self.assertEqual(res["failures"][0]["category"], bl.ARTIFACT_MISSING) + + def test_inventory_unregistered_never_fails(self): + m = _manifest(10, status="unregistered") + res = bl.compare(m, {"schema_version": 1, "budgets": []}, {"schema_version": 1, "measurements": []}) + self.assertEqual(res["failures"], []) + self.assertEqual(res["unregistered"][0]["artifact_id"], "pkg/bin/t/default/release") + + +class StrictSchema(unittest.TestCase): + def _load(self, text): + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as fh: + fh.write(text) + return bl.load_json(fh.name) + + def test_rejects_float(self): + with self.assertRaises(bl.SchemaError): + self._load('{"a": 1.5}') + + def test_rejects_duplicate_key(self): + with self.assertRaises(bl.SchemaError): + self._load('{"a": 1, "a": 2}') + + def test_rejects_negative_and_bool(self): + for v in (-1, True): + m = _manifest(10) + m["artifacts"][0]["metrics"]["raw"]["product_cap_bytes"] = v + with self.assertRaises(bl.SchemaError): + bl.validate_manifest(m) + + def test_rejects_unknown_key_and_dup_id_and_glob(self): + m = _manifest(10) + m["artifacts"][0]["extra"] = 1 + with self.assertRaises(bl.SchemaError): + bl.validate_manifest(m) + m = _manifest(10) + m["artifacts"].append(dict(m["artifacts"][0])) + with self.assertRaises(bl.SchemaError): + bl.validate_manifest(m) + m = _manifest(10) + m["artifacts"][0]["target"] = "*" + with self.assertRaises(bl.SchemaError): + bl.validate_manifest(m) + + def test_unregistered_requires_reason(self): + m = _manifest(10, status="unregistered") + del m["artifacts"][0]["reason"] + with self.assertRaises(bl.SchemaError): + bl.validate_manifest(m) + + +class Measurer(unittest.TestCase): + def test_raw_packed_unpacked(self): + with tempfile.TemporaryDirectory() as d: + d = Path(d) + (d / "f.bin").write_bytes(b"x" * 123) + with zipfile.ZipFile(d / "p.whl", "w") as z: + z.writestr("a", b"1" * 10) + z.writestr("b/c", b"2" * 5) + m = _manifest(10**6) + a2 = dict(m["artifacts"][0], kind="wheel", path="p.whl", + metrics={"packed": {"product_cap_bytes": 10**6}, "unpacked": {"product_cap_bytes": 10**6}}) + m["artifacts"].append(a2) + rep = bl.measure(bl.validate_manifest(m), d)["measurements"] + self.assertEqual(rep[0]["metrics"], {"raw": 123}) + self.assertEqual(rep[1]["metrics"]["unpacked"], 15) + self.assertEqual(rep[1]["metrics"]["packed"], (d / "p.whl").stat().st_size) + + def test_rejects_traversal_and_duplicates(self): + with tempfile.TemporaryDirectory() as d: + p = Path(d) / "bad.tgz" + with tarfile.open(p, "w:gz") as tf: + ti = tarfile.TarInfo("../evil") + ti.size = 1 + tf.addfile(ti, io.BytesIO(b"x")) + with self.assertRaises(bl.SchemaError): + bl._unpacked_bytes(p) + p = Path(d) / "dup.tgz" + with tarfile.open(p, "w:gz") as tf: + for _ in range(2): + ti = tarfile.TarInfo("a") + ti.size = 1 + tf.addfile(ti, io.BytesIO(b"x")) + with self.assertRaises(bl.SchemaError): + bl._unpacked_bytes(p) + + +class RepositoryInventory(unittest.TestCase): + """The checked-in inventory maps onto real .github/workflows jobs.""" + + def setUp(self): + self.m = bl.validate_manifest(bl.load_json(REPO / "quality/artifacts.json")) + bl.validate_budgets(bl.load_json(REPO / "quality/size-budgets.json"), self.m) + + def test_every_mapped_artifact_resolves_to_a_job(self): + self.assertEqual(bl.check_inventory(self.m, REPO), []) + + def test_every_release_upload_or_publish_job_is_inventoried(self): + jobs = bl.workflow_jobs((REPO / ".github/workflows/release.yml").read_text()) + producing = {j for j, (_, body) in jobs.items() + if "upload-artifact" in body or "publish" in body} + producing.discard("github-release") # aggregates the uploads above + used = {a["workflow"]["job"] for a in self.m["artifacts"] if a["status"] == "mapped"} + self.assertEqual(sorted(producing - used), []) + + def test_bad_job_name_is_reported(self): + m = json.loads(json.dumps(self.m)) + m["artifacts"][0]["workflow"]["job"] = "no-such-job" + self.assertTrue(bl.check_inventory(m, REPO)) + + +if __name__ == "__main__": + unittest.main() From b3c19610307bf73389eab87dc74a10556cca7d6d Mon Sep 17 00:00:00 2001 From: Trey Kim Date: Mon, 12 Oct 2026 00:34:06 +0800 Subject: [PATCH 2/3] =?UTF-8?q?quality:=20Q0=20=E2=80=94=20'unregistered'?= =?UTF-8?q?=20only=20for=20a=20fixed=20in-code=20allowlist;=20no=20budget?= =?UTF-8?q?=20on=20unregistered;=20report-only=20still=20fails=20BUDGET=5F?= =?UTF-8?q?UNREGISTERED?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC-0038a §1 :17, §9.4 :377, §12 :428 (upstream #208 @ 11511b35). --- scripts/quality/budget_lib.py | 42 +++++++++++ scripts/quality/check-budgets.py | 6 +- scripts/quality/tests/test_q0.py | 119 +++++++++++++++++++++++++++++-- 3 files changed, 160 insertions(+), 7 deletions(-) diff --git a/scripts/quality/budget_lib.py b/scripts/quality/budget_lib.py index d3d1162be..dc3993f5a 100755 --- a/scripts/quality/budget_lib.py +++ b/scripts/quality/budget_lib.py @@ -25,6 +25,21 @@ ARTIFACT_MISSING = "ARTIFACT_MISSING" BUDGET_INVALID = "BUDGET_INVALID" +# Categories that --report-only must never let through (RFC-0038a §9.4: a +# missing exact B/H is "register first, not report-only"). +NEVER_REPORT_ONLY = frozenset({BUDGET_UNREGISTERED, BUDGET_INVALID}) + +# The only inventory entries allowed to carry status 'unregistered' (PHASES +# 1.9; completing them is unit 1.9b). This list lives in code, not config, so +# a config-only PR cannot flip a mapped artifact to 'unregistered' and skip +# the gate (RFC-0038a §1 :17, §12 :428). Shrinking it is fine; growing it is a +# code change that needs review. +UNREGISTERED_ALLOWLIST = frozenset({ + "aimux-java/checksum/jvm/default/release", + "aimux-kotlin/checksum/jvm/default/release", + "aimux-ffi/c-header/any/default/release", +}) + class SchemaError(ValueError): pass @@ -139,6 +154,11 @@ def validate_manifest(doc: dict) -> dict: raise SchemaError(f"{where}: unregistered artifact must not carry 'workflow'") if not a.get("reason"): raise SchemaError(f"{where}: unregistered artifact needs a 'reason'") + if aid not in UNREGISTERED_ALLOWLIST: + raise SchemaError( + f"{where}: {aid} may not be 'unregistered'; only the fixed " + f"allowlist in budget_lib.UNREGISTERED_ALLOWLIST may be" + ) else: raise SchemaError(f"{where}.status: must be mapped or unregistered") if "path" in a and "*" in a["path"]: @@ -154,6 +174,9 @@ def validate_budgets(doc: dict, manifest: dict) -> dict: for a in manifest["artifacts"] for metric, spec in a["metrics"].items() } + unregistered = { + artifact_id(a) for a in manifest["artifacts"] if a["status"] == STATUS_UNREGISTERED + } seen = set() for i, b in enumerate(doc["budgets"]): where = f"budgets[{i}]" @@ -169,6 +192,10 @@ def validate_budgets(doc: dict, manifest: dict) -> dict: seen.add(key) if key not in caps: raise SchemaError(f"{where}: {key} is not in the artifact manifest") + if b["artifact_id"] in unregistered: + raise SchemaError( + f"{where}: {b['artifact_id']} is 'unregistered' in the manifest but has a budget" + ) B = _require_bytes(b["baseline_bytes"], f"{where}.B") H = _require_bytes(b["hard_cap_bytes"], f"{where}.H") C = _require_bytes(b["product_cap_bytes"], f"{where}.C") @@ -356,6 +383,13 @@ def compare(manifest: dict, budgets: dict, report: dict) -> dict: for a in manifest["artifacts"]: aid = artifact_id(a) if a["status"] == STATUS_UNREGISTERED: + # Defence in depth for callers that skipped validation: only the + # allowlist may be skipped, and never while a budget exists. + has_budget = any(k[0] == aid for k in bud) + if aid not in UNREGISTERED_ALLOWLIST or has_budget: + for metric in a["metrics"]: + failures.append({"artifact_id": aid, "metric": metric, "category": BUDGET_INVALID}) + continue unregistered.append({"artifact_id": aid, "reason": a["reason"]}) continue for metric in a["metrics"]: @@ -378,3 +412,11 @@ def compare(manifest: dict, budgets: dict, report: dict) -> dict: else: failures.append({**rec, "category": BUDGET_EXCEEDED}) return {"failures": failures, "passed": passed, "unregistered": unregistered} + + +def exit_code(result: dict, report_only: bool) -> int: + """0 = pass. --report-only still fails on NEVER_REPORT_ONLY categories.""" + failures = result["failures"] + if report_only: + failures = [f for f in failures if f["category"] in NEVER_REPORT_ONLY] + return 1 if failures else 0 diff --git a/scripts/quality/check-budgets.py b/scripts/quality/check-budgets.py index 53dcedb79..0f84d2f3e 100755 --- a/scripts/quality/check-budgets.py +++ b/scripts/quality/check-budgets.py @@ -3,7 +3,9 @@ Passes iff every registered metric has actual <= H. Inventory entries marked 'unregistered' are listed and never fail (PHASES 1.9; completion is 1.9b). ---report-only is the Q0 transitional mode (RFC-0038a §12): print, exit 0. +--report-only is the Q0 transitional mode (RFC-0038a §12 :428): it lets +BUDGET_EXCEEDED / ARTIFACT_MISSING through but still exits 1 on +BUDGET_UNREGISTERED and BUDGET_INVALID (RFC-0038a §9.4 :377). """ import argparse, json, sys from pathlib import Path @@ -20,4 +22,4 @@ b = bl.validate_budgets(bl.load_json(a.budgets), m) res = bl.compare(m, b, bl.load_json(a.report)) print(json.dumps(res, indent=2)) -sys.exit(0 if a.report_only or not res["failures"] else 1) +sys.exit(bl.exit_code(res, a.report_only)) diff --git a/scripts/quality/tests/test_q0.py b/scripts/quality/tests/test_q0.py index 15724aa16..46b7ef731 100644 --- a/scripts/quality/tests/test_q0.py +++ b/scripts/quality/tests/test_q0.py @@ -2,6 +2,7 @@ import io import json +import subprocess import sys import tarfile import tempfile @@ -16,9 +17,14 @@ REPO = HERE.parents[2] -def _manifest(cap, status="mapped", metric="raw"): +HEADER = ("aimux-ffi", "c-header", "any") # in UNREGISTERED_ALLOWLIST +HEADER_ID = "aimux-ffi/c-header/any/default/release" +SCRIPTS = HERE.parent + + +def _manifest(cap, status="mapped", metric="raw", ident=("pkg", "bin", "t")): a = { - "package": "pkg", "kind": "bin", "target": "t", "feature_set": "default", + "package": ident[0], "kind": ident[1], "target": ident[2], "feature_set": "default", "build_variant": "release", "metrics": {metric: {"product_cap_bytes": cap}}, "status": status, "path": "f.bin", } @@ -119,10 +125,113 @@ def test_missing_measurement_is_artifact_missing(self): self.assertEqual(res["failures"][0]["category"], bl.ARTIFACT_MISSING) def test_inventory_unregistered_never_fails(self): - m = _manifest(10, status="unregistered") + m = bl.validate_manifest(_manifest(10, status="unregistered", ident=HEADER)) res = bl.compare(m, {"schema_version": 1, "budgets": []}, {"schema_version": 1, "measurements": []}) self.assertEqual(res["failures"], []) - self.assertEqual(res["unregistered"][0]["artifact_id"], "pkg/bin/t/default/release") + self.assertEqual(res["unregistered"][0]["artifact_id"], HEADER_ID) + + +def _write(d, name, doc): + p = Path(d) / name + p.write_text(json.dumps(doc)) + return str(p) + + +def _cli(d, manifest, budgets, report, *extra): + return subprocess.run( + [sys.executable, str(SCRIPTS / "check-budgets.py"), + "--manifest", _write(d, "m.json", manifest), + "--budgets", _write(d, "b.json", budgets), + "--report", _write(d, "r.json", report), *extra], + capture_output=True, text=True, + ) + + +class UnregisteredBypass(unittest.TestCase): + """A mapped artifact must not escape the gate by being relabelled + 'unregistered' (RFC-0038a §1 :17, §9.4 :377, §12 :428).""" + + def test_allowlist_is_exactly_the_three(self): + self.assertEqual(bl.UNREGISTERED_ALLOWLIST, frozenset({ + "aimux-java/checksum/jvm/default/release", + "aimux-kotlin/checksum/jvm/default/release", + "aimux-ffi/c-header/any/default/release", + })) + + def test_repo_unregistered_set_equals_allowlist(self): + m = bl.load_json(REPO / "quality/artifacts.json") + got = {bl.artifact_id(a) for a in m["artifacts"] if a["status"] == "unregistered"} + self.assertEqual(got, set(bl.UNREGISTERED_ALLOWLIST)) + + def test_non_allowlisted_unregistered_is_rejected(self): + with self.assertRaises(bl.SchemaError): + bl.validate_manifest(_manifest(10, status="unregistered")) + + def test_every_repo_mapped_item_flipped_is_rejected(self): + m = bl.load_json(REPO / "quality/artifacts.json") + for i, a in enumerate(m["artifacts"]): + if a["status"] != "mapped": + continue + mut = json.loads(json.dumps(m)) + x = mut["artifacts"][i] + x["status"] = "unregistered" + del x["workflow"] + x["reason"] = "bypass attempt" + with self.assertRaises(bl.SchemaError, msg=bl.artifact_id(a)): + bl.validate_manifest(mut) + + def test_allowlisted_unregistered_with_budget_is_rejected(self): + m = bl.validate_manifest(_manifest(10**6, status="unregistered", ident=HEADER)) + b = _budgets(1000, 10**6) + b["budgets"][0]["artifact_id"] = HEADER_ID + with self.assertRaises(bl.SchemaError): + bl.validate_budgets(b, m) + # compare() also refuses if validation was skipped. + res = bl.compare(m, b, {"schema_version": 1, "measurements": []}) + self.assertEqual(res["failures"][0]["category"], bl.BUDGET_INVALID) + self.assertEqual(res["unregistered"], []) + + def test_exact_mutation_mapped_to_unregistered_keeps_budget_10e12_fails(self): + # Registered + budgeted item; flip to unregistered, keep budget, + # feed 10^12 bytes. + C = 10**9 + m = _manifest(C) + b = _budgets(1000, C) + rep = _report(10**12) + self.assertEqual(bl.compare(bl.validate_manifest(m), b, rep)["failures"][0]["category"], + bl.BUDGET_EXCEEDED) + a = m["artifacts"][0] + a["status"] = "unregistered" + del a["workflow"] + a["reason"] = "temporarily not measured" + with self.assertRaises(bl.SchemaError): + bl.validate_manifest(m) + res = bl.compare(m, b, rep) # validation skipped: still fails + self.assertTrue(res["failures"]) + self.assertEqual(res["passed"], []) + with tempfile.TemporaryDirectory() as d: + for extra in ((), ("--report-only",)): + p = _cli(d, m, b, rep, *extra) + self.assertNotEqual(p.returncode, 0, (extra, p.stdout, p.stderr)) + + def test_report_only_fails_on_budget_unregistered(self): + m = _manifest(10) + empty = {"schema_version": 1, "budgets": []} + res = bl.compare(m, empty, _report(1)) + self.assertEqual(res["failures"][0]["category"], bl.BUDGET_UNREGISTERED) + self.assertEqual(bl.exit_code(res, report_only=True), 1) + with tempfile.TemporaryDirectory() as d: + p = _cli(d, m, empty, _report(1), "--report-only") + self.assertEqual(p.returncode, 1, p.stdout + p.stderr) + + def test_report_only_lets_size_exceeded_through_but_normal_mode_fails(self): + m, b, rep = _manifest(10**9), _budgets(1000, 10**9), _report(1101) + res = bl.compare(m, b, rep) + self.assertEqual(bl.exit_code(res, report_only=True), 0) + self.assertEqual(bl.exit_code(res, report_only=False), 1) + with tempfile.TemporaryDirectory() as d: + self.assertEqual(_cli(d, m, b, rep, "--report-only").returncode, 0) + self.assertEqual(_cli(d, m, b, rep).returncode, 1) class StrictSchema(unittest.TestCase): @@ -161,7 +270,7 @@ def test_rejects_unknown_key_and_dup_id_and_glob(self): bl.validate_manifest(m) def test_unregistered_requires_reason(self): - m = _manifest(10, status="unregistered") + m = _manifest(10, status="unregistered", ident=HEADER) del m["artifacts"][0]["reason"] with self.assertRaises(bl.SchemaError): bl.validate_manifest(m) From 5620aa17449bafd30d5a5e57c3d819459904661e Mon Sep 17 00:00:00 2001 From: Trey Kim Date: Mon, 12 Oct 2026 00:40:25 +0800 Subject: [PATCH 3/3] docs(q0): align unregistered comments with allowlist behavior --- scripts/quality/budget_lib.py | 3 ++- scripts/quality/check-budgets.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/quality/budget_lib.py b/scripts/quality/budget_lib.py index dc3993f5a..f4dc79528 100755 --- a/scripts/quality/budget_lib.py +++ b/scripts/quality/budget_lib.py @@ -372,7 +372,8 @@ def compare(manifest: dict, budgets: dict, report: dict) -> dict: - mapped artifact, budget present, measured: pass iff actual <= H - mapped artifact, budget present, not measured: ARTIFACT_MISSING - mapped artifact, no budget: BUDGET_UNREGISTERED - - inventory status 'unregistered': listed, never a failure (1.9b) + - inventory status 'unregistered': listed and skipped only if the ID is in + UNREGISTERED_ALLOWLIST and has no budget; otherwise BUDGET_INVALID (1.9b) """ measured = {} for r in report["measurements"]: diff --git a/scripts/quality/check-budgets.py b/scripts/quality/check-budgets.py index 0f84d2f3e..83d0dc5dd 100755 --- a/scripts/quality/check-budgets.py +++ b/scripts/quality/check-budgets.py @@ -2,7 +2,8 @@ """Compare a size report against registered budgets (RFC-0038a §3.2, §9.1). Passes iff every registered metric has actual <= H. Inventory entries marked -'unregistered' are listed and never fail (PHASES 1.9; completion is 1.9b). +'unregistered' are skipped only if on UNREGISTERED_ALLOWLIST with no budget; +any other unregistered entry fails as BUDGET_INVALID (PHASES 1.9; completion is 1.9b). --report-only is the Q0 transitional mode (RFC-0038a §12 :428): it lets BUDGET_EXCEEDED / ARTIFACT_MISSING through but still exits 1 on BUDGET_UNREGISTERED and BUDGET_INVALID (RFC-0038a §9.4 :377).