From fc489bf42a7ffc826efefa01a1338ab4e1ef320d Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Wed, 20 May 2026 08:22:36 -0500 Subject: [PATCH 1/7] feat(scripts): add F2/F10 evidence parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure ES-module parser for fenced ```evidence``` blocks under checked verification items. Implements the 2026-05-19 amendment shape: - Per-block flat YAML (command/location/result/timestamp, optional check) - location: ci|local|manual - local: command must contain a recognised executable token - ci: command (or check:) must match a successful check-run on head SHA - timestamps: future guard >now+5min; local/manual must be >=head-5min - ci timestamp drift >5min vs check-run completed_at is a warning Parser has no SDK or network dependencies — the caller injects already- fetched data via the function signature. Hand-rolled key:value parser avoids adding a YAML dep. Also adds a minimal repo-root package.json with vitest as the sole devDep so the parser can be unit-tested in CI. Reported as a repo-contract change. Refs #10 Refs #12 --- .gitignore | 1 + package-lock.json | 1426 ++++++++++++++++++++++++++++++++++++ package.json | 11 + scripts/parse-evidence.mjs | 230 ++++++ 4 files changed, 1668 insertions(+) create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/parse-evidence.mjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8561704 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1426 @@ +{ + "name": "github-workflows", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "github-workflows", + "devDependencies": { + "vitest": "^2.1.9" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4db81c8 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "github-workflows", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run" + }, + "devDependencies": { + "vitest": "^2.1.9" + } +} diff --git a/scripts/parse-evidence.mjs b/scripts/parse-evidence.mjs new file mode 100644 index 0000000..e2f493d --- /dev/null +++ b/scripts/parse-evidence.mjs @@ -0,0 +1,230 @@ +// Pure ES-module parser for F2 + F10 verification evidence blocks. +// +// Contract (per ArchonVII/github-workflows#10 / #12 amendment 2026-05-19): +// Each checked verification item `- [x] ` must be followed by exactly +// one fenced block labelled ```evidence ... ``` with flat YAML-style keys: +// command: string (required) +// location: ci | local | manual (required) +// result: string (required) +// timestamp: ISO-8601 string (required) +// check: string (optional; for ci rows when command name != check-run name) +// +// This module is intentionally pure: it accepts already-fetched PR data via +// its function signature and performs no network or SDK calls. SDK access +// happens in the workflow caller (actions/github-script) which then passes +// results into parseEvidence(). +// +// No external dependencies — uses only Node stdlib. The evidence block is +// small and flat, so a hand-rolled key:value parser is sufficient and avoids +// pulling in a YAML dependency. + +// Allow-list of plausible executable tokens for `location: local` commands. +// Whole-word, case-insensitive matching. Source: amendment 2026-05-19, B4. +const LOCAL_TOKENS = [ + 'npm', 'pnpm', 'yarn', 'pytest', 'uv', 'python', 'node', 'gh', 'git', + 'npx', 'actionlint', 'tsc', 'eslint', 'ruff', 'cargo', 'go', 'make', + 'bash', 'pwsh', 'deno', 'vitest', +]; + +const VALID_LOCATIONS = new Set(['ci', 'local', 'manual']); + +// 5-minute clock-skew tolerance in milliseconds. Source: amendment C2/C3. +const SKEW_MS = 5 * 60 * 1000; + +/** + * Parse the PR body and validate every checked verification claim. + * + * @param {string} prBody + * @param {object} ctx + * @param {string} ctx.headCommitTime - ISO timestamp of PR head commit. + * @param {string} ctx.now - ISO timestamp injected for determinism. + * @param {Array<{name:string, completed_at:string, conclusion:string}>} ctx.checkRuns + * @returns {{ok: boolean, warnings: string[], errors: string[]}} + */ +export function parseEvidence(prBody, { headCommitTime, now, checkRuns }) { + const warnings = []; + const errors = []; + + const headMs = Date.parse(headCommitTime); + const nowMs = Date.parse(now); + + const lines = (prBody || '').split(/\r?\n/); + + // Walk lines, collecting `- [x] ...` and `- [ ] ...` items and the + // (optional) immediately-following ```evidence``` block. + for (let i = 0; i < lines.length; i++) { + const m = lines[i].match(/^\s*-\s+\[([ xX])\]\s+(.*)$/); + if (!m) continue; + + const checked = m[1].toLowerCase() === 'x'; + const claim = m[2].trim(); + + // Look ahead for an `evidence` fenced block, skipping blank lines. + let j = i + 1; + while (j < lines.length && lines[j].trim() === '') j++; + + const fenceOpen = j < lines.length + ? lines[j].match(/^\s*```\s*evidence\s*$/i) + : null; + + if (!checked) { + if (fenceOpen) { + warnings.push(`Unchecked item has an evidence block (ignored): "${claim}"`); + } else { + warnings.push(`Unchecked verification item (no evidence required): "${claim}"`); + } + continue; + } + + if (!fenceOpen) { + errors.push(`Checked item "${claim}" is missing a fenced \`\`\`evidence\`\`\` block.`); + continue; + } + + // Find the closing fence. + let k = j + 1; + while (k < lines.length && !/^\s*```\s*$/.test(lines[k])) k++; + if (k >= lines.length) { + errors.push(`Checked item "${claim}" has an unterminated evidence block.`); + continue; + } + + const blockLines = lines.slice(j + 1, k); + + // Detect a second evidence block under the same item (illegal). + let n = k + 1; + while (n < lines.length && lines[n].trim() === '') n++; + if (n < lines.length && /^\s*```\s*evidence\s*$/i.test(lines[n])) { + errors.push(`Checked item "${claim}" has more than one evidence block.`); + // Skip ahead past the second block to avoid double-reporting. + let p = n + 1; + while (p < lines.length && !/^\s*```\s*$/.test(lines[p])) p++; + i = p; + continue; + } + + const parsed = parseFlatYaml(blockLines); + if (parsed.error) { + errors.push(`Evidence block for "${claim}" is malformed: ${parsed.error}`); + i = k; + continue; + } + + validateEvidence(claim, parsed.data, { headMs, nowMs, checkRuns }, errors, warnings); + i = k; + } + + return { ok: errors.length === 0, warnings, errors }; +} + +// Hand-rolled flat YAML parser for the evidence block. +// Accepts: `key: value` per line. Quoted values (single/double) are unquoted. +// Comments (`# ...`) and blank lines are ignored. Indentation is ignored. +// Anything else is a syntax error. +function parseFlatYaml(blockLines) { + const data = {}; + for (const raw of blockLines) { + const line = raw.replace(/\s+$/, ''); + if (line.trim() === '') continue; + if (/^\s*#/.test(line)) continue; + const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/); + if (!m) { + return { error: `unparseable line: ${JSON.stringify(line)}` }; + } + let value = m[2].trim(); + // Strip a trailing comment if not inside quotes. + if (value.startsWith('"') || value.startsWith("'")) { + const q = value[0]; + const end = value.indexOf(q, 1); + if (end === -1) { + return { error: `unterminated string for key "${m[1]}"` }; + } + value = value.slice(1, end); + } + if (value === '') { + return { error: `empty value for key "${m[1]}"` }; + } + data[m[1]] = value; + } + return { data }; +} + +function validateEvidence(claim, ev, { headMs, nowMs, checkRuns }, errors, warnings) { + const requiredKeys = ['command', 'location', 'result', 'timestamp']; + for (const key of requiredKeys) { + if (!(key in ev)) { + errors.push(`Evidence for "${claim}" missing required key: ${key}`); + return; + } + } + + const { command, location, timestamp } = ev; + + if (!VALID_LOCATIONS.has(location)) { + errors.push(`Evidence for "${claim}" has unknown location "${location}" (allowed: ci, local, manual).`); + return; + } + + // Timestamp parse (always required; even ci uses prose timestamp for drift check). + const tsMs = Date.parse(timestamp); + if (Number.isNaN(tsMs)) { + errors.push(`Evidence for "${claim}" has invalid ISO-8601 timestamp: ${JSON.stringify(timestamp)}`); + return; + } + + // Future-stamp guard applies to all locations. + if (tsMs > nowMs + SKEW_MS) { + errors.push(`Evidence for "${claim}" has a future timestamp (${timestamp}).`); + return; + } + + if (location === 'local') { + if (!hasLocalToken(command)) { + errors.push( + `Evidence for "${claim}" has location=local but command "${command}" contains no recognised executable token (${LOCAL_TOKENS.join(', ')}).`, + ); + return; + } + if (tsMs < headMs - SKEW_MS) { + errors.push(`Evidence for "${claim}" predates PR head commit (timestamp ${timestamp} < head ${new Date(headMs).toISOString()}).`); + return; + } + } else if (location === 'manual') { + // Manual evidence has no token requirement; still must be post-head. + if (tsMs < headMs - SKEW_MS) { + errors.push(`Evidence for "${claim}" predates PR head commit (timestamp ${timestamp} < head ${new Date(headMs).toISOString()}).`); + return; + } + } else if (location === 'ci') { + const wanted = ev.check || command; + const run = (checkRuns || []).find((r) => r.name === wanted); + if (!run) { + errors.push(`Evidence for "${claim}" references CI check "${wanted}" which is not a check-run on the PR head SHA.`); + return; + } + if (run.conclusion !== 'success') { + errors.push(`Evidence for "${claim}" references CI check "${wanted}" with conclusion "${run.conclusion}" (expected success).`); + return; + } + // CI authority: ignore prose timestamp for correctness, but warn on drift. + const completedMs = Date.parse(run.completed_at); + if (!Number.isNaN(completedMs) && Math.abs(tsMs - completedMs) > SKEW_MS) { + warnings.push( + `Evidence for "${claim}" prose timestamp drifts >5min from check-run completed_at (${timestamp} vs ${run.completed_at}).`, + ); + } + } +} + +function hasLocalToken(command) { + const lc = command.toLowerCase(); + return LOCAL_TOKENS.some((tok) => { + // Whole-word match: token is bordered by non-word chars or string ends. + const re = new RegExp(`(^|[^a-z0-9_])${escapeRegex(tok)}([^a-z0-9_]|$)`); + return re.test(lc); + }); +} + +function escapeRegex(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} From ddafed7a7e56f2aded869a00e2551da1d197b1b8 Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Wed, 20 May 2026 08:22:43 -0500 Subject: [PATCH 2/7] test(scripts): add vitest suite for parse-evidence 15 cases covering all amendment-2026-05-19 requirements: happy paths (local/ci/manual), structural errors (missing block, double block, malformed YAML, unknown location), command validation (no token, missing check-run, failed check-run), and timestamp validation (future, pre-head, ci drift warning, unchecked, invalid ISO). Refs #10 Refs #12 --- scripts/parse-evidence.test.mjs | 228 ++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 scripts/parse-evidence.test.mjs diff --git a/scripts/parse-evidence.test.mjs b/scripts/parse-evidence.test.mjs new file mode 100644 index 0000000..93f3f24 --- /dev/null +++ b/scripts/parse-evidence.test.mjs @@ -0,0 +1,228 @@ +import { describe, it, expect } from 'vitest'; +import { parseEvidence } from './parse-evidence.mjs'; + +// Fixed times for deterministic tests. +// HEAD commit is at 2026-05-19T12:00:00Z; "now" is 1 hour later. +const HEAD = '2026-05-19T12:00:00Z'; +const NOW = '2026-05-19T13:00:00Z'; + +// Helper: build a body with one checked item + one fenced evidence block. +function withEvidence(claim, block, opts = {}) { + const checked = opts.checked === false ? ' ' : 'x'; + return [ + `- [${checked}] ${claim}`, + '', + '```evidence', + ...block, + '```', + '', + ].join('\n'); +} + +const baseCtx = (over = {}) => ({ + headCommitTime: HEAD, + now: NOW, + checkRuns: [], + ...over, +}); + +describe('parseEvidence — happy paths', () => { + it('1. valid local — passes', () => { + const body = withEvidence('Tests pass', [ + 'command: npm test', + 'location: local', + 'result: pass: 19/19', + 'timestamp: 2026-05-19T12:30:00Z', + ]); + const r = parseEvidence(body, baseCtx()); + expect(r.errors).toEqual([]); + expect(r.ok).toBe(true); + }); + + it('2. valid ci with matching successful check-run — passes', () => { + const body = withEvidence('CI green', [ + 'command: ci-success', + 'location: ci', + 'result: pass', + 'timestamp: 2026-05-19T12:45:00Z', + ]); + const r = parseEvidence(body, baseCtx({ + checkRuns: [{ name: 'ci-success', completed_at: '2026-05-19T12:45:00Z', conclusion: 'success' }], + })); + expect(r.errors).toEqual([]); + expect(r.ok).toBe(true); + }); + + it('3. valid manual — passes', () => { + const body = withEvidence('Screenshot reviewed', [ + 'command: manual smoke — sidebar resize looks correct', + 'location: manual', + 'result: pass: screenshot attached', + 'timestamp: 2026-05-19T12:30:00Z', + ]); + const r = parseEvidence(body, baseCtx()); + expect(r.errors).toEqual([]); + expect(r.ok).toBe(true); + }); +}); + +describe('parseEvidence — structural errors', () => { + it('4. checked item with no fenced block — error', () => { + const body = '- [x] Tests pass\n\nSome other text.\n'; + const r = parseEvidence(body, baseCtx()); + expect(r.ok).toBe(false); + expect(r.errors[0]).toMatch(/missing a fenced/); + }); + + it('5. two fenced blocks under one item — error', () => { + const body = [ + '- [x] Tests pass', + '', + '```evidence', + 'command: npm test', + 'location: local', + 'result: pass', + 'timestamp: 2026-05-19T12:30:00Z', + '```', + '', + '```evidence', + 'command: npm run lint', + 'location: local', + 'result: pass', + 'timestamp: 2026-05-19T12:31:00Z', + '```', + '', + ].join('\n'); + const r = parseEvidence(body, baseCtx()); + expect(r.ok).toBe(false); + expect(r.errors.some((e) => /more than one evidence block/.test(e))).toBe(true); + }); + + it('6. malformed YAML (missing colon) — error', () => { + const body = withEvidence('Tests pass', [ + 'command npm test', + 'location: local', + 'result: pass', + 'timestamp: 2026-05-19T12:30:00Z', + ]); + const r = parseEvidence(body, baseCtx()); + expect(r.ok).toBe(false); + expect(r.errors[0]).toMatch(/malformed|unparseable/); + }); + + it('7. unknown location value — error', () => { + const body = withEvidence('Tests pass', [ + 'command: npm test', + 'location: cloud', + 'result: pass', + 'timestamp: 2026-05-19T12:30:00Z', + ]); + const r = parseEvidence(body, baseCtx()); + expect(r.ok).toBe(false); + expect(r.errors[0]).toMatch(/unknown location/); + }); +}); + +describe('parseEvidence — command validation', () => { + it('8. local with command "ran tests" (no token) — error', () => { + const body = withEvidence('Tests pass', [ + 'command: ran tests', + 'location: local', + 'result: pass', + 'timestamp: 2026-05-19T12:30:00Z', + ]); + const r = parseEvidence(body, baseCtx()); + expect(r.ok).toBe(false); + expect(r.errors[0]).toMatch(/no recognised executable token/); + }); + + it('9. ci with command not in checkRuns — error', () => { + const body = withEvidence('CI green', [ + 'command: nonexistent-check', + 'location: ci', + 'result: pass', + 'timestamp: 2026-05-19T12:45:00Z', + ]); + const r = parseEvidence(body, baseCtx({ checkRuns: [] })); + expect(r.ok).toBe(false); + expect(r.errors[0]).toMatch(/not a check-run/); + }); + + it('10. ci with check-run conclusion failure — error', () => { + const body = withEvidence('CI green', [ + 'command: ci-success', + 'location: ci', + 'result: pass', + 'timestamp: 2026-05-19T12:45:00Z', + ]); + const r = parseEvidence(body, baseCtx({ + checkRuns: [{ name: 'ci-success', completed_at: '2026-05-19T12:45:00Z', conclusion: 'failure' }], + })); + expect(r.ok).toBe(false); + expect(r.errors[0]).toMatch(/conclusion "failure"/); + }); +}); + +describe('parseEvidence — timestamp validation', () => { + it('11. future timestamp (> now + 5min) — error', () => { + const body = withEvidence('Tests pass', [ + 'command: npm test', + 'location: local', + 'result: pass', + // NOW is 13:00:00Z; +1h is well beyond +5min. + 'timestamp: 2026-05-19T14:00:00Z', + ]); + const r = parseEvidence(body, baseCtx()); + expect(r.ok).toBe(false); + expect(r.errors[0]).toMatch(/future timestamp/); + }); + + it('12. pre-head timestamp (local) — error', () => { + const body = withEvidence('Tests pass', [ + 'command: npm test', + 'location: local', + 'result: pass', + // HEAD is 12:00Z; an hour earlier is way before head - 5min. + 'timestamp: 2026-05-19T11:00:00Z', + ]); + const r = parseEvidence(body, baseCtx()); + expect(r.ok).toBe(false); + expect(r.errors[0]).toMatch(/predates PR head/); + }); + + it('13. ci prose timestamp drifts >5min from completed_at — warning, ok:true', () => { + const body = withEvidence('CI green', [ + 'command: ci-success', + 'location: ci', + 'result: pass', + // 20 min before check-run completed_at; drift warning. + 'timestamp: 2026-05-19T12:25:00Z', + ]); + const r = parseEvidence(body, baseCtx({ + checkRuns: [{ name: 'ci-success', completed_at: '2026-05-19T12:45:00Z', conclusion: 'success' }], + })); + expect(r.ok).toBe(true); + expect(r.errors).toEqual([]); + expect(r.warnings.some((w) => /drifts >5min/.test(w))).toBe(true); + }); + + it('14. unchecked box with no evidence — warning, ok:true', () => { + const body = '- [ ] Tests pass\n'; + const r = parseEvidence(body, baseCtx()); + expect(r.ok).toBe(true); + expect(r.errors).toEqual([]); + expect(r.warnings.some((w) => /Unchecked/.test(w))).toBe(true); + }); + + it('15. invalid ISO timestamp — error', () => { + const body = withEvidence('Tests pass', [ + 'command: npm test', + 'location: local', + 'result: pass', + 'timestamp: not-a-date', + ]); + const r = parseEvidence(body, baseCtx()); + expect(r.ok).toBe(false); + expect(r.errors[0]).toMatch(/invalid ISO-8601/); + }); +}); From 7eeb66287d4e77ec67b565d50d91837d5ff54bf5 Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Wed, 20 May 2026 08:29:51 -0500 Subject: [PATCH 3/7] feat(pr-policy): wire F2/F10 evidence parser (warning-only) Adds an `enforce-evidence` boolean input (default false for Phase 1) and a new evidence-check step in the existing policy job that: - skips on draft PRs (advisory) and doc-only PRs (existing detection) - fetches check-runs for the PR head SHA via github.rest.checks.listForRef - fetches the head commit time via github.rest.git.getCommit - dynamic-imports scripts/parse-evidence.mjs from a sibling checkout of ArchonVII/github-workflows (so callers don't need the script vendored) - writes errors and warnings into $GITHUB_STEP_SUMMARY - only calls core.setFailed when enforce-evidence === true AND errors exist A comment block above the step documents that Owner Maintenance Lane is direct-commit-only in Phase 1 and intentionally has no PR-path exemption. Refs #10 Refs #12 --- .github/workflows/pr-policy.yml | 136 ++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/.github/workflows/pr-policy.yml b/.github/workflows/pr-policy.yml index 315014e..1bfcf64 100644 --- a/.github/workflows/pr-policy.yml +++ b/.github/workflows/pr-policy.yml @@ -45,6 +45,15 @@ on: description: "Also run actionlint on the PR." type: boolean default: true + enforce-evidence: + description: | + Phase 1 default is false: the F2/F10 evidence parser runs but only + emits warnings to the job summary; malformed/missing evidence does + NOT fail the workflow. Phase 2+ callers may opt in by setting this + to true, in which case errors from scripts/parse-evidence.mjs hard- + fail the job. + type: boolean + default: false jobs: policy: @@ -52,6 +61,17 @@ jobs: permissions: pull-requests: read steps: + # Check out THIS reusable workflow's repo (ArchonVII/github-workflows) + # into a sibling path so the evidence-check step can dynamic-import + # scripts/parse-evidence.mjs. We deliberately don't check out the caller + # repo here — the existing policy step uses the GitHub API and doesn't + # need a working tree. + - name: Check out github-workflows for parser script + uses: actions/checkout@v4 + with: + repository: ArchonVII/github-workflows + path: __github-workflows__ + - name: Enforce ready-for-review PR policy uses: actions/github-script@v7 env: @@ -114,6 +134,122 @@ jobs: core.setFailed(failures.join('\n')); } + # ---------------------------------------------------------------------- + # F2 + F10 evidence parser (amendment 2026-05-19). + # + # NOTE: Owner Maintenance Lane is direct-commit-only in Phase 1 and + # never produces a PR, so it cannot reach this workflow. No PR-path + # exemption for owner-maintenance is implemented here on purpose — + # do NOT add a label/author check without a corresponding policy + # update. The exemption lives upstream in the commit path. + # ---------------------------------------------------------------------- + - name: Evidence check (warning-only by default) + uses: actions/github-script@v7 + env: + DOC_EXT_LIST: ${{ inputs.doc-only-extensions }} + DOC_PREFIXES: ${{ inputs.doc-only-path-prefixes }} + ENFORCE_EVIDENCE: ${{ inputs.enforce-evidence }} + with: + script: | + const pr = context.payload.pull_request; + const summary = core.summary; + + if (pr.draft) { + core.info('Draft PR; evidence parser is advisory.'); + await summary + .addHeading('Evidence check (advisory)', 3) + .addRaw('Draft PR — evidence parser skipped.') + .write(); + return; + } + + // Share the doc-only detection with the existing policy step. + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + per_page: 100, + }); + const docExtRe = new RegExp(`\\.(${process.env.DOC_EXT_LIST})$`, 'i'); + const prefixes = process.env.DOC_PREFIXES + .split('\n') + .map((s) => s.trim()) + .filter(Boolean); + const isDocFile = (p) => docExtRe.test(p) || prefixes.some((pre) => p.startsWith(pre)); + if (files.length > 0 && files.every((f) => isDocFile(f.filename))) { + core.info('Doc-only PR; evidence parser skipped.'); + await summary + .addHeading('Evidence check', 3) + .addRaw('Doc-only PR — evidence parser skipped.') + .write(); + return; + } + + // Fetch check-runs for the PR head SHA (parser authority for ci rows). + const checkRunsResp = await github.paginate( + github.rest.checks.listForRef, + { + owner: context.repo.owner, + repo: context.repo.repo, + ref: pr.head.sha, + per_page: 100, + }, + ); + const checkRuns = checkRunsResp.map((r) => ({ + name: r.name, + completed_at: r.completed_at, + conclusion: r.conclusion, + })); + + // Head commit time. context.payload.pull_request only carries the + // head SHA; fetch the commit to get its committer date. + const headCommit = await github.rest.git.getCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: pr.head.sha, + }); + const headCommitTime = headCommit.data.committer.date; + + // Dynamic-import the pure parser so this step has no module wiring. + const path = require('path'); + const url = require('url'); + const parserPath = path.resolve( + process.env.GITHUB_WORKSPACE, + '__github-workflows__/scripts/parse-evidence.mjs', + ); + const { parseEvidence } = await import(url.pathToFileURL(parserPath).href); + + const result = parseEvidence(pr.body || '', { + headCommitTime, + now: new Date().toISOString(), + checkRuns, + }); + + await summary.addHeading('Evidence check', 3).write(); + if (result.errors.length > 0) { + await summary + .addHeading('Errors', 4) + .addList(result.errors) + .write(); + for (const e of result.errors) core.warning(`evidence: ${e}`); + } + if (result.warnings.length > 0) { + await summary + .addHeading('Warnings', 4) + .addList(result.warnings) + .write(); + for (const w of result.warnings) core.warning(`evidence: ${w}`); + } + if (result.errors.length === 0 && result.warnings.length === 0) { + await summary.addRaw('All checked verification items have well-formed evidence.').write(); + } + + if (process.env.ENFORCE_EVIDENCE === 'true' && result.errors.length > 0) { + core.setFailed(`Evidence parser found ${result.errors.length} error(s).`); + } else if (result.errors.length > 0) { + core.info('enforce-evidence is false; reporting errors as warnings only.'); + } + actionlint: if: inputs.run-actionlint runs-on: ubuntu-latest From 7f30269186e03311767c56bdea42d01bb0230930 Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Wed, 20 May 2026 08:30:09 -0500 Subject: [PATCH 4/7] ci(self-test): run vitest on PRs touching scripts/ Triggers on PR / push-to-main when scripts/, package.json, package-lock.json, or the workflow itself changes. Uses Node 20 with the built-in npm cache, npm ci, then `npm test` which invokes vitest run. Refs #10 Refs #12 --- .github/workflows/self-test.yml | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/self-test.yml diff --git a/.github/workflows/self-test.yml b/.github/workflows/self-test.yml new file mode 100644 index 0000000..be75ec8 --- /dev/null +++ b/.github/workflows/self-test.yml @@ -0,0 +1,43 @@ +name: Self-test (scripts) + +# Non-reusable workflow that runs the github-workflows repo's own unit +# tests when scripts/** changes on a PR or push to main. Other reusable +# workflows in this repo are workflow_call-only and don't run here. + +on: + pull_request: + paths: + - "scripts/**" + - "package.json" + - "package-lock.json" + - ".github/workflows/self-test.yml" + push: + branches: [main] + paths: + - "scripts/**" + - "package.json" + - "package-lock.json" + - ".github/workflows/self-test.yml" + +concurrency: + group: self-test-${{ github.ref }} + cancel-in-progress: true + +jobs: + vitest: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install + run: npm ci + + - name: Run vitest + run: npm test From 8425e098ae759a07b5b3292ce04aab64774df450 Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Wed, 20 May 2026 13:20:33 -0500 Subject: [PATCH 5/7] feat(parser): add grep/awk/sed/jq to local-token list Source: PR #19 review patch 1 (2026-05-20). Adds common shell utility tokens so 'location: local' evidence rows that invoke grep/awk/sed/jq parse cleanly under enforce mode. --- scripts/parse-evidence.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/parse-evidence.mjs b/scripts/parse-evidence.mjs index e2f493d..9d033b6 100644 --- a/scripts/parse-evidence.mjs +++ b/scripts/parse-evidence.mjs @@ -24,6 +24,10 @@ const LOCAL_TOKENS = [ 'npm', 'pnpm', 'yarn', 'pytest', 'uv', 'python', 'node', 'gh', 'git', 'npx', 'actionlint', 'tsc', 'eslint', 'ruff', 'cargo', 'go', 'make', 'bash', 'pwsh', 'deno', 'vitest', + // Shell utility tokens — added 2026-05-20 per PR #19 review patch 1. + // Guards against dogfood regression where a `grep` evidence row would + // hard-fail under enforce mode. + 'grep', 'awk', 'sed', 'jq', ]; const VALID_LOCATIONS = new Set(['ci', 'local', 'manual']); From 741c33d4da9a9f71e7007253ea62bd94b28874b2 Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Wed, 20 May 2026 13:20:37 -0500 Subject: [PATCH 6/7] test(parser): add grep evidence regression case Source: PR #19 review patch 1 (2026-05-20). Directly guards against the dogfood regression where 'grep -nE ...' would hard-fail under enforce mode. 16/16 passing. --- scripts/parse-evidence.test.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/parse-evidence.test.mjs b/scripts/parse-evidence.test.mjs index 93f3f24..e755b4c 100644 --- a/scripts/parse-evidence.test.mjs +++ b/scripts/parse-evidence.test.mjs @@ -214,6 +214,21 @@ describe('parseEvidence — timestamp validation', () => { expect(r.warnings.some((w) => /Unchecked/.test(w))).toBe(true); }); + it('16. local with grep command — passes (regression: PR #19 dogfood)', () => { + // Source: PR #19 review patch 1 (2026-05-20). Guards against the dogfood + // regression where `grep -nE "ERROR" build.log` would hard-fail under + // enforce mode because `grep` wasn't in LOCAL_TOKENS. + const body = withEvidence('Log scanned for errors', [ + 'command: grep -nE "ERROR" build.log', + 'location: local', + 'result: pass: 0 matches', + 'timestamp: 2026-05-19T12:30:00Z', + ]); + const r = parseEvidence(body, baseCtx()); + expect(r.errors).toEqual([]); + expect(r.ok).toBe(true); + }); + it('15. invalid ISO timestamp — error', () => { const body = withEvidence('Tests pass', [ 'command: npm test', From 2246b4a363ff7b379be3880685e0affcec42915b Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Wed, 20 May 2026 13:21:04 -0500 Subject: [PATCH 7/7] feat(pr-policy): add workflow-library-ref input for parser checkout Source: PR #19 review patch 2 (2026-05-20). Pins the ArchonVII/github-workflows checkout that supplies parse-evidence.mjs to the same ref the caller pins this reusable workflow to. Default v1 keeps parser+workflow versioned together; callers using @main or @sha should pass workflow-library-ref to match. Previously the checkout had no ref and defaulted to the default branch (main), creating 'workflow from v1, parser from main' drift. --- .github/workflows/pr-policy.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/pr-policy.yml b/.github/workflows/pr-policy.yml index 1bfcf64..e84ec7a 100644 --- a/.github/workflows/pr-policy.yml +++ b/.github/workflows/pr-policy.yml @@ -54,6 +54,16 @@ on: fail the job. type: boolean default: false + workflow-library-ref: + description: | + Git ref (tag, branch, or SHA) of ArchonVII/github-workflows used to + source scripts/parse-evidence.mjs. MUST match the ref the caller + pins this reusable workflow to (e.g. @v1). Defaulting to v1 keeps + parser+workflow versioned together; callers using @main or a SHA + should pass workflow-library-ref to match. Source: PR #19 review + patch 2 (2026-05-20). + type: string + default: v1 jobs: policy: @@ -66,10 +76,18 @@ jobs: # scripts/parse-evidence.mjs. We deliberately don't check out the caller # repo here — the existing policy step uses the GitHub API and doesn't # need a working tree. + # + # Pin parser source to the same ref callers pin this reusable workflow + # to. Defaulting to v1 keeps parser+workflow versioned together; + # callers using @main or @sha should pass workflow-library-ref to + # match. Without an explicit ref, actions/checkout would default to + # the repository's default branch (main), creating "workflow from v1, + # parser from main" drift. - name: Check out github-workflows for parser script uses: actions/checkout@v4 with: repository: ArchonVII/github-workflows + ref: ${{ inputs.workflow-library-ref }} path: __github-workflows__ - name: Enforce ready-for-review PR policy