diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc new file mode 100644 index 0000000..dbf8ed4 --- /dev/null +++ b/.fallowrc.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json", + // Generated by `bun run generate:*` (verified by `generate:check`). + "ignorePatterns": ["packages/gkit/generated/**"], + // Local-only helper that renders .npmrc from the vault; not a dependency. + "ignoreBinaries": ["latch"], +} diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..28a389d --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# style: harness oxfmt +38c178094b782ba4326aa7cb47c27c2518e62e66 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c526db..cc90a37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: @@ -38,11 +43,8 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Check types - run: bun run check-types - - - name: Test - run: bun run test + - name: Check + run: bun run check - name: Evaluate agent contract run: bun run eval diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 64af56b..c03ce85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,6 +36,11 @@ jobs: fetch-tags: true persist-credentials: false + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: @@ -70,13 +75,9 @@ jobs: if: steps.preflight.outputs.release == 'true' run: bun install --frozen-lockfile - - name: Check types - if: steps.preflight.outputs.release == 'true' - run: bun run check-types - - - name: Test + - name: Check if: steps.preflight.outputs.release == 'true' - run: bun run test + run: bun run check - name: Evaluate agent contract if: steps.preflight.outputs.release == 'true' diff --git a/.gitignore b/.gitignore index 4761d02..09be76f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ coverage *.tgz .npmrc +.claude/worktrees/ diff --git a/.vite-hooks/pre-commit b/.vite-hooks/pre-commit new file mode 100755 index 0000000..85fb65b --- /dev/null +++ b/.vite-hooks/pre-commit @@ -0,0 +1 @@ +vp staged diff --git a/README.md b/README.md index 22072a2..efeab95 100644 --- a/README.md +++ b/README.md @@ -84,38 +84,38 @@ Provider execution must bind exactly one App profile. Create one JSON file at ```json { - "version": 1, - "name": "my-app", - "providers": { - "posthog": { - "config": { - "host": "https://us.posthog.com", - "projectId": "12345" - }, - "secrets": { - "apiToken": "env:MY_APP_POSTHOG_TOKEN" - } - }, - "gsc": { - "config": { - "siteUrl": "sc-domain:example.com" - }, - "secrets": { - "serviceAccountFile": "env:MY_APP_GSC_SERVICE_ACCOUNT_FILE" - } - }, - "hubspot": { - "config": {}, - "secrets": { - "accessToken": "env:MY_APP_HUBSPOT_ACCESS_TOKEN" - } - }, - "pagespeed": { - "secrets": { - "apiKey": "env:MY_APP_PAGESPEED_API_KEY" - } - } - } + "version": 1, + "name": "my-app", + "providers": { + "posthog": { + "config": { + "host": "https://us.posthog.com", + "projectId": "12345" + }, + "secrets": { + "apiToken": "env:MY_APP_POSTHOG_TOKEN" + } + }, + "gsc": { + "config": { + "siteUrl": "sc-domain:example.com" + }, + "secrets": { + "serviceAccountFile": "env:MY_APP_GSC_SERVICE_ACCOUNT_FILE" + } + }, + "hubspot": { + "config": {}, + "secrets": { + "accessToken": "env:MY_APP_HUBSPOT_ACCESS_TOKEN" + } + }, + "pagespeed": { + "secrets": { + "apiKey": "env:MY_APP_PAGESPEED_API_KEY" + } + } + } } ``` @@ -279,7 +279,6 @@ Development dependencies include `@celados/argc` from the private team registry ```bash bun run eval -bun run check-types -bun run test +bun run check bun run verify:package ``` diff --git a/bun.lock b/bun.lock index a20ee64..ae12df6 100644 --- a/bun.lock +++ b/bun.lock @@ -5,16 +5,20 @@ "": { "name": "gkit", "devDependencies": { + "@celados/harness": "^0.1.3", "@types/bun": "catalog:", "@types/node": "catalog:", "bun-types": "catalog:", + "fallow": "^3.42.0", + "tsrs": "0.24.0-ts7.1.0-beta", "typescript": "catalog:", + "vite-plus": "^1.6.0", "vitest": "catalog:", }, }, "packages/gkit": { "name": "gkit", - "version": "0.3.6", + "version": "0.5.0", "bin": { "gkit": "dist/gkit.js", }, @@ -31,7 +35,7 @@ "google-auth-library": "^10.9.0", "robots-parser": "3.0.1", "saxes": "6.0.0", - "typescript": "^5", + "typescript": "^7.0.2", "valibot": "^1.2.0", "vitest": "^4.0.18", "yaml": "^2.9.0", @@ -39,26 +43,142 @@ }, }, "catalog": { - "@celados/argc": "^11.0.0", "@types/bun": "^1.3.10", "@types/node": "^25.6.0", - "@valibot/to-json-schema": "^1.6.0", - "ajv": "^8.20.0", "bun-types": "^1.3.13", - "dataforseo-client": "^2.0.25", - "dotenv": "^17.2.2", - "google-auth-library": "^10.9.0", - "typescript": "^5", - "valibot": "^1.2.0", + "typescript": "^7.0.2", "vitest": "^4.0.18", - "yaml": "^2.9.0", }, "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/runtime": ["@babel/runtime@7.29.14", "", {}, "sha512-Y5tqevQaUAZhhj82BpYMNC1ssvLQTVAfwKdm8iy8qwmhvC2/CpcAu8OC+QEtjM7pMk7knx83u1Qj8gzkicyelw=="], + + "@blazediff/core": ["@blazediff/core@1.10.0", "", {}, "sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ=="], + "@celados/argc": ["@celados/argc@11.0.0", "https://npm.celados.com/@celados/argc/-/@celados/argc-11.0.0.tgz", { "dependencies": { "yaml": "^2.9.1" }, "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@valibot/to-json-schema": "^1.8.0", "valibot": "^1.5.0" } }, "sha512-376S0EpKHXV00RLGmQuMfuYYZBLgWIAHngpdmGAbGAn5FBhHNeKd5Lg9XNdv/qtTfIwlONOLnfyt4QTM1Gw3ZA=="], + "@celados/harness": ["@celados/harness@0.1.3", "https://npm.celados.com/@celados/harness/-/@celados/harness-0.1.3.tgz", { "peerDependencies": { "@celados/glaze": ">=0.33.0", "@convex-dev/eslint-plugin": ">=5.0.0", "@effect/tsgo": ">=0.68.0", "@gdp-ts/core": ">=0.1.0", "@stylexjs/eslint-plugin": ">=0.19.0", "eslint-plugin-solid": ">=0.19.0", "fallow": ">=3.42.0", "tsrs": ">=0.24.0-ts7.1.0-beta", "typescript": ">=7.0.2", "vite-plus": ">=1.6.0" }, "optionalPeers": ["@celados/glaze", "@convex-dev/eslint-plugin", "@effect/tsgo", "@gdp-ts/core", "@stylexjs/eslint-plugin", "eslint-plugin-solid"], "bin": { "harness": "dist/cli.mjs" } }, "sha512-FFdYryrXOTBiCSaHweAprzWOiSMIc7KPWsDn+iGOiDfD2R7j1xf02Emr+eXVme50kPrfQPZeAFwAj7Bw/SNXOA=="], + + "@fallow-cli/darwin-arm64": ["@fallow-cli/darwin-arm64@3.42.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xxmT9R8jj+6Uc7eaIJSz5rb8fbUl90Qah+1XL0EpQWplr4oVugQs1pueHiiYDz013D93bwLVqe4y//zEtNWWtg=="], + + "@fallow-cli/darwin-x64": ["@fallow-cli/darwin-x64@3.42.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UXiXJPezGjkRYZK2f1ns7lC5erQC9add0CLqaEX0xSsvAU5nKVToVr4ibCq6+FMfeiBkc/7txUfpwG9/khJwIg=="], + + "@fallow-cli/linux-arm64-gnu": ["@fallow-cli/linux-arm64-gnu@3.42.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qdUkcj/6pRQaUsLPT3yPimjQa+9J0b6QrzT+sV8Rngs7iihzej3cBMM/yqxoT9U7SsHiErvGIr2y6codUu321Q=="], + + "@fallow-cli/linux-arm64-musl": ["@fallow-cli/linux-arm64-musl@3.42.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-50JlZzhu6x+OnddmdhDwIEgtNdns2vxTkW4svI3kafFqZxBoW6gkYj0Lgy0I6he/3TmEiHk5GbJ8m+UtPTUzRg=="], + + "@fallow-cli/linux-x64-gnu": ["@fallow-cli/linux-x64-gnu@3.42.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9Hcg0sapABeAsY+2lanr0JyAZHucXMta+V0fAfqtCe7J7jR/oNc0xocO849lNt6s4Jc59TcOxajZArJZC7DRWQ=="], + + "@fallow-cli/linux-x64-musl": ["@fallow-cli/linux-x64-musl@3.42.0", "", { "os": "linux", "cpu": "x64" }, "sha512-fMg7FJogrZx/qaoZEXxk7gDrWnXmFvQibaR1+hZz661qjqf04Ejr9+I5ymLKgNu9oWC2oy3kyqjYlNLrrO0fBA=="], + + "@fallow-cli/win32-arm64-msvc": ["@fallow-cli/win32-arm64-msvc@3.42.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-g/4FbRnli52w5fyZBuA9Tyj9AyRk5xWEwwkkmidd6Wz8s5IypO7VFXrX/RnSIfp583WJ1dDubtBe2/vTJtRccg=="], + + "@fallow-cli/win32-x64-msvc": ["@fallow-cli/win32-x64-msvc@3.42.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/Cg7/te1DqFNivekXoI2Eg8V97nTugyeAd0/p0TO5MGSw3L0fN/8RwtgcCX7LTDlFLs0wEh8/Af0zfCsZFD49Q=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.3", "", {}, "sha512-lhTtjtoEArWBjismtfS761S9Hlyn+nIZqOZVYoZ1Xn82PMNMVnk+jj/NhC4fAXGTs7ydONcJiR5vyQBTMDu27g=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@oxc-project/types": ["@oxc-project/types@0.144.0", "", {}, "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@oxc-project/runtime": ["@oxc-project/runtime@0.158.0", "", {}, "sha512-OYWBlK/wnZHyol5jkvBJadD8KTR6DJfFDRClqrU6kBDQKHd4bXNZALg3qfXk0aFl6F5loSxMT24EZurNWE4MKQ=="], + + "@oxc-project/types": ["@oxc-project/types@0.158.0", "", {}, "sha512-xZ66qA4WmLbTLlxH3mqMd8f9UKbaOzOT0ATwFf4DH/14pMgpUrtqtjyYzOQmgfl2a0iRAAMbQ9G1th/67jRzGw=="], + + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.78.0", "", { "os": "android", "cpu": "arm" }, "sha512-1x9NPGQG5WMOOT8spRP4hPbeqwB/P9ayxFo4EUDSle6mLmtYRqE6suXgHr93/S2nnszy77LlIa8klkKdWtHbMw=="], + + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.78.0", "", { "os": "android", "cpu": "arm64" }, "sha512-1muYaW9SiPEq8NIPGX0B20oUwCx6HWiCsZem/2jU7UsOzoXLT4YxWq6ybdhwKSKl0PyAfvyMyPF7FsI1To7AdA=="], + + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.78.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jl4KJdo8HrVpLmcSYS/7oDXPH8Hzd6VrzgUFRAbY0j6cSRjcDkUnpuAStuNLoqvMtZEi7Rdy68BBriCgWJcy2g=="], + + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.78.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-RwOhQgAQi1qV3TeuTfrgGkCWEL3eDFIql/lo4rt2xpGHqyolKa+W5cFbb+M9IvwMwQbplii5Gbv1ZQQPTJlNug=="], + + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.78.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-MAXydj01mLjMUlnmo7sYVm+7kdBdU9Nwaux2Q1f9oLNqt3CCdego1oMoZ30qDyZQGQXfVJZEU1yRV7mSzwKDVA=="], + + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.78.0", "", { "os": "linux", "cpu": "arm" }, "sha512-rz6FVZRiWsq3O2YQgCoqZRwaQQaPQ70NBks4+KINjjPJOjBTd8g/iq6ynS97ode+qKGlhXtpc/xCl8LHzfOjyQ=="], + + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.78.0", "", { "os": "linux", "cpu": "arm" }, "sha512-lIK0XJi4JP1FnhuLjR/JWhQHGghZc+ghFNnbZTQC8kxsmrkW3ex8CeF7fp0FT+rEP0Nm6voCYkasL8poVWA9sQ=="], + + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.78.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-o2LJIHfW3ZADOjDVk5c7MM1M8CiAG43GTc4mlJAK23fOHgru4CvnqWedCoCXpIsKokoRdEa7XdQywPGZv1BAiA=="], + + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.78.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-G+lUaQk94Vo+4byyd8X2+pPXtd7wObpdbvBC8xyozZD0ROSVBI3XpNNR6HECkJq4Q51AwLuJG81f4x+4eqvSrw=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.78.0", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-r76PAZf9/ZgF/iDRaIf8nLsr4x6ZHe/j2QLmWUjoQ46Sa11qMO68a5u5iux3y8vLzqYTS/xtKO9PjjMfyk5png=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.78.0", "", { "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-SQiXTt/c4aVoRXkJvhf4Y/sA1XYe3XjSMDbdX5PiAssBuHERoGtncQXz0KKWkXPejWTKlAL4mZSQZdHOCiw7TA=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.78.0", "", { "os": "linux", "cpu": "none", "libc": "musl" }, "sha512-VNB3wFUx/rAqwVNW+uyXxb+c0HUrmcS51UPkFMDWJY95c6Tck6u6HOyVLsqiYoC8kh2mGlwanYmjb/R2McvN0A=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.78.0", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-1nbI0Y7SXwx2f4mJmKt58MaDdjAcoaYM90pBMGdcZCsTW2taFW1/MUi3uQGRPAN2n+3PldCdO9m+D6x+gU/heg=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.78.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-I2z2ISPjrl3CSRF5fmQkNnoh46UVU1GjM1V0+COJyNZcj0SDNM4DrPP2/gXCX38YJ1P2BL6sF2VdsJ+pEYEbJQ=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.78.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-YHt5nKKFxT4SruI57FHnEhGRSDuoFqNoAM3moYCClg70mjjS39gBBbdzaZv+N0otdzvQVuHTPu3jFu5tpqC3XQ=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.78.0", "", { "os": "none", "cpu": "arm64" }, "sha512-WH21lBMU+LYzF7aMeozTZlyc44oa2F+I5OtQ7RwuidCDoAxgTyN1s34rx0PBYrio56+2G88U9VOnYJsyV7VMHQ=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.78.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-uNQqHJrtHIiSl8pMbhDktmGpA1hqwldWNO8kbNpNFhVt+BYk4C3Ge8nlJgkRtGjtMam0+542G5UQJJG6lLePbg=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.78.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-W6omURRT39EHBPH4Z8sUPIOzWVjMbhCCCWcfT0sa6aeC+vOXNyBAJmpQRh5//KfPDX96jJlNith9i4N65wxB+Q=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.78.0", "", { "os": "win32", "cpu": "x64" }, "sha512-r9rWKeDVc7lIrj0Us9kP08W7FudYAoWyEdO40qnKk20bHiTbW/z2v4LoIxxUENQyFfApbeV2Gl9XsEgY/tY1dA=="], + + "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@7.0.2006", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uc0MEHXZUDW23Zmo5X28eSNeSnP17jKnh9GiehpVZqhfy99ks8spyXT5Llar3tjDN7ppnQN0Qd9FK6WNkdD+fQ=="], + + "@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@7.0.2006", "", { "os": "darwin", "cpu": "x64" }, "sha512-+x1/pa3S3VMC9cif8XKTOCQUuqRT+wu1Ix+FK5svXa2xbXCuE7+4S0SfmuSnK/mk9AI5+xBxZFhILxmCPUZ1hA=="], + + "@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@7.0.2006", "", { "os": "linux", "cpu": "arm64" }, "sha512-90v7HX8m54kHXn5DpjNEN18Fb7xwJMREe/wji3USyAf9pTMv7a3qZ3cPoLViIcIzxPLmkcYZIs1j1H3Z2gbVLA=="], + + "@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@7.0.2006", "", { "os": "linux", "cpu": "x64" }, "sha512-/k0wsEqHiA5dWBJYuld3Vecv6bIhKCasxpho/aFkvh870Qox33MuTkKUGWIVm/njAxw7W4pu1a/Xfa3jwuKuPQ=="], + + "@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@7.0.2006", "", { "os": "win32", "cpu": "arm64" }, "sha512-CQT5sqcLZLNZSWLzUUEjQmp74UX8h1agp3ZJ8pfRktHbx6gfBa8a7CjrnRrZ9VXlwnXVkpw7m5KBO6y+S5MQRA=="], + + "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@7.0.2006", "", { "os": "win32", "cpu": "x64" }, "sha512-VZsGowXcp3vQNkjoGeVCOTf4aGrRpRk6bBJWhefnJVPr6z+dX+LKqqHfwaqnFvg66Q2iJpXqklRW38YO21ee+Q=="], + + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.93.0", "", { "os": "android", "cpu": "arm" }, "sha512-KKw70pbD5Pvq3G2lJ+WQ71XPMjCmg37rkSe2+mDqCRP3PoTSdDMuEi4UOnE8JBtyNM7bek61EC2s0Jt7S7u+Sg=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.93.0", "", { "os": "android", "cpu": "arm64" }, "sha512-fvr1fTUZGelwRxKDt64pf28aL1WaWgviwz3X+E/FcRYvrlEahH0NECRz/c1SY+K+KWmZk5uAf6h1kQnyBkp/2A=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.93.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VdvSN9JRf4Es6ZzoAdvEeh8tgKdw8L4H82WmA8MMPVfPdZGCwE/nI27Z1HrRHYQhPxOh4kmnxQEioSUFrOvDaw=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.93.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-QR1V890Ml0YGRG0F/iV0K9igG+pOhe+VWd+aZnxY849rRHZf3F2gEoQks5+GVHf105X9Mh/6HdShTP5juekG2g=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.93.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jDfIbi30zZaKi+rTvig3sge+Xhi8pFFSgHxb0jzovc4qh8ETm/irKV9c7N/JsuwnYc5sbvf4IC8w1vQQ0IL+BA=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.93.0", "", { "os": "linux", "cpu": "arm" }, "sha512-qtxZ714ND/koFgVH00QgxlzZiIbRPmguaPhnHRU5DFFtrtEdOrSnZD4U/H2AzXrXkZ59eQbMBnJwbum5XohrIQ=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.93.0", "", { "os": "linux", "cpu": "arm" }, "sha512-5pJ9NjMvH7WMUqXB6bTTOSemBm39w2cod27Hu3+Yom/GhbTeNXlYcpDx1o4CgBP6fhp0YHG66TNdVaUm6sHjsA=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.93.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-pRf+7jtbt2fHE23SX7qBxRgnBZicUgsoCK1PVIjnOk+t3a6UBkpt/GrKI99WlxsKUxftz/5nv/kfKvScmYywkw=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.93.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-zM8Fd/K8sqGvsBcJBZ3oVZ3ctGWbszA08IaGt5gFFF5RcunppAI/ZnAXbouABvnWeJ//S44KKlfPZnDDbeiphg=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.93.0", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-7qKCnvhZKDXJG2pdbLazd2hw4FxjJHbZOFrrbzyy5PdHfPLH9vnNUa1le01A8uItZuYKPBPncH+HCoEiFT4SgA=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.93.0", "", { "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-2pgulxC6G8V5wE750NJE/fRkf9aO6i6yoj10wyaR2EdiNtan4OemsOtsQ0ZTXGdY8Q3IoTmlND3GYvjJsvikYg=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.93.0", "", { "os": "linux", "cpu": "none", "libc": "musl" }, "sha512-yWsn6Gg2PnGG5jlyDz5+pHGiMTMiy4adaa4WRMzDgzB8efaShk1JMmTHIBSVLQMh0tYXRs4CN5uJCO79r4Mi4g=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.93.0", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-WCn5plwp/ezS2RDPQKp0/EXEVRr9/dDexUdRKmsMFqzNM5xBSZ1wbsi3PuMk1VfkMRnI5BuazCsziCWMXRDOlw=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.93.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-F2CGjb9LAkzH+IYTZSzd1klQ1vk5z6Utjax64WeLByCkYXbVDbsun+Oy2hLfW2MDSKCvW+jwy/9ld0EPgdeu+A=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.93.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-plf5DE2objcG8Tf7mXRuRa8tT8rzDkpjqXQtKoGsKHWkpgsialbON1+cCtoSEUTVbQUjXvuAm8vV9+3rR0yCBw=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.93.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Kp2yJrKvoTe9FfKdO/xwLNUbs2bYTMKx0+aoQD1s0h/Tx0kSgtHSdGmrk8tTKdluosjFbo88qYRIaNIX43AfFw=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.93.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-vigjcHCFsR6KjnWTEVfaCxcMYapEuaK9FZa1MpewwQ1S+Xud3v0okfkj1RySsD6RF1NRHIjSlQSalTnKIaj3nA=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.93.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0Ot3BEe/AAr2/uQhe55r8CmPlperYk2mQmxdutOru7oU0kir6IiEVHb1TG3GORRlfra97lPHVDLhgmihiKzLrg=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.93.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZzzIIVs/Qkm8trUWYKF0OYub5ALlv50/3gsEZbKAvWgz+yNJX6uRmQ8ySPXU2Q0/Y9m4JCOjhpmC8XUYbPHX5Q=="], + + "@oxlint/plugins": ["@oxlint/plugins@1.93.0", "", {}, "sha512-bKWQI1g9jn8bWUZsvhbOLfgva576Fbp1KwoTNSUCMdT9tlTAvQS2JkifX1UrhPPdyN4GHvM20HhvIMqqU1yotQ=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA=="], @@ -92,6 +212,26 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@testing-library/dom": ["@testing-library/dom@10.4.2", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.10", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-tA4stGNp3mLGOGO+lh87UA56Tz4c8NVGBSkyIF58O+W/bs2Re9BCI9xihnL70+DsThncFznE0skS88SM1D2hyg=="], + + "@tsrs/darwin-arm64": ["@tsrs/darwin-arm64@0.24.0-ts7.1.0-beta", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GxGPRieCGIXevJgGlbne9pRqkfAN+VmBQvBHWWcF8rXLNX9yCxYo78/a7Auqjf723b4eFTlM5mgkclUit/Fu9w=="], + + "@tsrs/darwin-x64": ["@tsrs/darwin-x64@0.24.0-ts7.1.0-beta", "", { "os": "darwin", "cpu": "x64" }, "sha512-TrXdOyEq++blyYp6KphoHGmsGxAn1flYzjm0peLEI3c88z1UQa+pKkcuPgZ6FOxTW6fSaT9572n21kz+0bWi/g=="], + + "@tsrs/linux-arm64-gnu": ["@tsrs/linux-arm64-gnu@0.24.0-ts7.1.0-beta", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-Ph5NK0vnaR4gnBW2LgrZsUPx4mKF+9tCQUYZFOHmny6SMojSsm/5GCiBu0sHZEZhmyIqq8UbineUEA7T0QImnw=="], + + "@tsrs/linux-arm64-musl": ["@tsrs/linux-arm64-musl@0.24.0-ts7.1.0-beta", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-vjgx8wGuAVSOPLlb2eVQpQVolImZa7vsH/1kvYOaokcP/KZXVUmHzgroxK0t9gxJ0SXvT4VKNjWZ+t+4tDNu1A=="], + + "@tsrs/linux-x64-gnu": ["@tsrs/linux-x64-gnu@0.24.0-ts7.1.0-beta", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-tj1SNWBnJoBUzU2zrD18tUu3h6o36FXQGPpmIXsADHSfU5H9pVQdK/QzPIO76rb72P7foXHluE+xGTXXf8nNaA=="], + + "@tsrs/linux-x64-musl": ["@tsrs/linux-x64-musl@0.24.0-ts7.1.0-beta", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-sizu0EdGw9ITqEaZKg60TDhDk5PXT9RzVkCdesBjk/BZhS0BVU2cRhvoRBmDbk+2vCUgW14j+Otg3tl+002qNQ=="], + + "@tsrs/win32-x64": ["@tsrs/win32-x64@0.24.0-ts7.1.0-beta", "", { "os": "win32", "cpu": "x64" }, "sha512-WAbzgd5JBOauQ/oL6DTdGEh09tM/q8tlGAcRORskhPK7eIQVVa8Js2pc7DVW0XokWTSADeD9v6HKtU2Ge0r/fQ=="], + + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -102,26 +242,114 @@ "@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="], + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + "@valibot/to-json-schema": ["@valibot/to-json-schema@1.7.1", "", { "peerDependencies": { "valibot": "^1.4.0" } }, "sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A=="], "@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="], - "@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="], + "@vitest/mocker": ["@vitest/mocker@5.1.0", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@vitest/spy": "5.1.0", "estree-walker": "^3.0.3", "magic-string": "^1.4.3" }, "peerDependencies": { "msw": "^2.4.9 || ^3.0.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + "@vitest/pretty-format": ["@vitest/pretty-format@5.1.0", "", { "dependencies": { "tinyrainbow": "^3.2.0" } }, "sha512-IRx5mc30CrFzyMNni6SZKQ1r53kv049Mcvvk0WbWcD6PUz/bFjG4zYv0KosQAeonPpGp5WxqYBMqdSOfJfvVWw=="], "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="], - "@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], + "@vitest/snapshot": ["@vitest/snapshot@5.1.0", "", { "dependencies": { "@vitest/pretty-format": "5.1.0", "@vitest/utils": "5.1.0", "magic-string": "^1.4.3", "pathe": "^2.0.3" } }, "sha512-6tLM70+37LVXz+A6QmOlMQUuj+taODAyXZhz58lNNNNKHH0D81yzPBEezKQQGPlABc8OpAQBOBVvssVLLQqixw=="], + + "@vitest/spy": ["@vitest/spy@5.1.0", "", {}, "sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A=="], + + "@vitest/utils": ["@vitest/utils@5.1.0", "", { "dependencies": { "@vitest/pretty-format": "5.1.0", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.2.0" } }, "sha512-V+qgCwkvVw+s1t1S5TaoBHsLi5Sz/YmohCMoOu0mZ8ZQst2bKhRYARAQucWpVg8D0mv0x5MHcWXY5QdOsoiGBQ=="], + + "@voidzero-dev/vite-plus-darwin-arm64": ["@voidzero-dev/vite-plus-darwin-arm64@1.6.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1IiE9eXsnLWesaHEI2l1L38+CChaQ82WJd3GpF+Ud7Kuq+T2x8TebUwLCPYatpTSDDs8z2w1yKAl7quoVZhetA=="], + + "@voidzero-dev/vite-plus-darwin-x64": ["@voidzero-dev/vite-plus-darwin-x64@1.6.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-oSsfR1ACQdFo60cuvInK5AZxPx7TAKyrt7NsUaNVZVj24ALsh+0hjZSRmWkJ3lF8aRtw1Lh8d7bkFXgQcQdKxQ=="], + + "@voidzero-dev/vite-plus-linux-arm64-gnu": ["@voidzero-dev/vite-plus-linux-arm64-gnu@1.6.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-6m2vRctdtQDl+ARQwGTF9X6hg+JaVFtj4vkvBLy/dWpqiJzseGebJwKUjys8QuhXgcJocn3a+N6oh27vvfINUQ=="], + + "@voidzero-dev/vite-plus-linux-arm64-musl": ["@voidzero-dev/vite-plus-linux-arm64-musl@1.6.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-ZHx0byqPOQ/WiAxHbfdvTp8HICWAzKje8kfZ6ihLu9ZLQ1nOo0Ri5FAgYTAmLMfMXgU5qZuNFQFdp4k5ONe3jQ=="], + + "@voidzero-dev/vite-plus-linux-x64-gnu": ["@voidzero-dev/vite-plus-linux-x64-gnu@1.6.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-de7/qKZN6y7G2OMnsCcmBXeXyS39G4r5uyiz8jRbxbObj2nqxmlNs48aHqOaKzqiJeSqip+z4ALY/AIAl6h4rw=="], + + "@voidzero-dev/vite-plus-linux-x64-musl": ["@voidzero-dev/vite-plus-linux-x64-musl@1.6.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-e+/9mDyhBeoDEwNPt4x8ubHztTsQE9kNHI9cBaE0Y+KIQPTxeV2W3OfPHfI5yTlKWtF8r3KN6kIRlXgaFvkBsw=="], + + "@voidzero-dev/vite-plus-win32-arm64-msvc": ["@voidzero-dev/vite-plus-win32-arm64-msvc@1.6.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-xo4Ha1xDkmWp8LolKUtCsVifenjK3ReQNuJjcMt1PazAdd9QZR/Qn6Z8E3rbFjnWctSkhxR4wTd43z8Bkxb9vA=="], + + "@voidzero-dev/vite-plus-win32-x64-msvc": ["@voidzero-dev/vite-plus-win32-x64-msvc@1.6.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0tC6Bv1FT6Qz9YikQepOibsZa2HE0kxYSIK1jReQh2/3dBUGz2tHib5+G2rQ7ebkHxmwS9gQHAeMmm07XfemSQ=="], + + "@yuku-core/binding-android-arm64": ["@yuku-core/binding-android-arm64@0.19.0", "", { "os": "android", "cpu": "arm64" }, "sha512-wdL2XmTiZixAOaHk9Z0JJZ8u9Qm8d333Zjpc6gaxXFX+UR5BlsfxZ7a2+2Rm71pamppsPYGUGFq2oVLbh6DISw=="], + + "@yuku-core/binding-darwin-arm64": ["@yuku-core/binding-darwin-arm64@0.19.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7sNJa6XJhyetnTy64jw3NEi4YgUcezHk1G5GH9FSvXAuC8wIlbaOwT9lgq5qo5sspjrZJQlBR1TWflP49uRpMw=="], + + "@yuku-core/binding-darwin-x64": ["@yuku-core/binding-darwin-x64@0.19.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-pTC7dYEc59isdyizc3nsuyFNBu2LU5wMYZ1be6UMr59PY/AJNtO1aBLWV0MgL/z7C1nv/2sX4TNjGgKLoeevJw=="], + + "@yuku-core/binding-freebsd-x64": ["@yuku-core/binding-freebsd-x64@0.19.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xrZjF6XOEwyLOiDIfrwDdRCS6ghxcJeirNC1KivQhVcUwlRonaU79S3/Uiyx3lBylPnT8qKJ3/aMT+H52nGTJQ=="], + + "@yuku-core/binding-linux-arm-gnu": ["@yuku-core/binding-linux-arm-gnu@0.19.0", "", { "os": "linux", "cpu": "arm", "libc": "glibc" }, "sha512-0OyIH53C0I1CbADHn9FSdCfX5qbpeLwJhmOVhonFJNYheybOnkSJ50qGMfTJIb8y5zrwqvLs7FVBvz6Fk8maZg=="], + + "@yuku-core/binding-linux-arm-musl": ["@yuku-core/binding-linux-arm-musl@0.19.0", "", { "os": "linux", "cpu": "arm", "libc": "musl" }, "sha512-z/iPfkCOCSXE34VXNt0uP1ORSW+4mEa9b5CiJcWZeqZH08g49bFU/jRM2T04f+Wpw5mJ6HIzNBiojxeAbrHjeg=="], + + "@yuku-core/binding-linux-arm64-gnu": ["@yuku-core/binding-linux-arm64-gnu@0.19.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-jLNTi+gzaRwBJ+gwhCJ+rDb5dmx/8hEBx93UWd7hjMSqPTL1D1wvVSvXVAldV5kALvju7b0/m51EHEhs4+I1fA=="], + + "@yuku-core/binding-linux-arm64-musl": ["@yuku-core/binding-linux-arm64-musl@0.19.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-YwyPIdW/rN2Fmv0MTFWi47FaB3F9g+2b+sJISyAw83Yd60RWH4T3Nx6XkAHHVN+uKJNBAd6So7/BVihSiFYvuQ=="], + + "@yuku-core/binding-linux-x64-gnu": ["@yuku-core/binding-linux-x64-gnu@0.19.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-/vskaAXVX3ESR43FiTdfiYS7z1b9uoQJwVQ/0RgUHgN2sWoAyCZs+aX4CgU7eKGIHTVfp9VFwv+c+2XchYiEtQ=="], + + "@yuku-core/binding-linux-x64-musl": ["@yuku-core/binding-linux-x64-musl@0.19.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-FuXL5igeSKib9Rye02TdH+D6SQezTyGxIATMu1Qsn/QywrzwpGl35OLpS2C1x5vpGLeVE2XgiLd5mVPFHmIglQ=="], + + "@yuku-core/binding-win32-arm64": ["@yuku-core/binding-win32-arm64@0.19.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YuJAe7zEKT1Xa7LuHlhGJpPnYQeVtvo0rZvNjJRqkH0hQGOw4C8QeP30Tg5VKGzZfo2x8CjhjEhKt4TmLwfmdg=="], - "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], + "@yuku-core/binding-win32-x64": ["@yuku-core/binding-win32-x64@0.19.0", "", { "os": "win32", "cpu": "x64" }, "sha512-o8iY/mXwlRkedp0LNTr4+TEfXTPQwoNZmv6k+1WIAd1ZXEn8xJJTBCtB0osaVvdZo/x/cNmV16UrWrrGpM5Mvw=="], - "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + "@yuku-toolchain/types": ["@yuku-toolchain/types@0.19.0", "", {}, "sha512-tFkNbsP4WXX9UMUC5syJGdK9dPMLK0PPaIuXCcCYH75sZ8MrebfBZ2XvCILRTw8AgLivSSCfDa4R67iYBDa4Kg=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], @@ -134,7 +362,7 @@ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chai": ["chai@6.3.0", "", {}, "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg=="], "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], @@ -150,8 +378,12 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -176,6 +408,10 @@ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fallow": ["fallow@3.42.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@fallow-cli/darwin-arm64": "3.42.0", "@fallow-cli/darwin-x64": "3.42.0", "@fallow-cli/linux-arm64-gnu": "3.42.0", "@fallow-cli/linux-arm64-musl": "3.42.0", "@fallow-cli/linux-x64-gnu": "3.42.0", "@fallow-cli/linux-x64-musl": "3.42.0", "@fallow-cli/win32-arm64-msvc": "3.42.0", "@fallow-cli/win32-x64-msvc": "3.42.0", "fallow-type-aware": "3.42.0" }, "bin": { "fallow": "bin/fallow", "fallow-lsp": "bin/fallow-lsp", "fallow-mcp": "bin/fallow-mcp" } }, "sha512-zVug6ujNPVVW706qDL5OZvjD+tINA/lav+PwaoM5yIZyLs2y4bspTZBqSggUi56AGpLuHDEerIJAYTIgQzHQfQ=="], + + "fallow-type-aware": ["fallow-type-aware@3.42.0", "", { "dependencies": { "typescript": "7.0.2" }, "peerDependencies": { "tsrs": "*" }, "optionalPeers": ["tsrs"], "bin": { "fallow-type-aware": "fallow-type-aware.mjs" } }, "sha512-TxCPA7ZGyTi30UQ3I20ybBrno6hWCOuxItd4g8GW8HkGrfwhepIsjgM1iK2Njv7rD8lgYHiIq+uVrHquXYm40w=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], @@ -184,6 +420,10 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + + "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -204,6 +444,8 @@ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -236,11 +478,15 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + "nanoid": ["nanoid@3.3.20", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="], "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], @@ -250,6 +496,12 @@ "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], + "oxfmt": ["oxfmt@0.78.0", "", { "dependencies": { "tinypool": "2.2.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.78.0", "@oxfmt/binding-android-arm64": "0.78.0", "@oxfmt/binding-darwin-arm64": "0.78.0", "@oxfmt/binding-darwin-x64": "0.78.0", "@oxfmt/binding-freebsd-x64": "0.78.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.78.0", "@oxfmt/binding-linux-arm-musleabihf": "0.78.0", "@oxfmt/binding-linux-arm64-gnu": "0.78.0", "@oxfmt/binding-linux-arm64-musl": "0.78.0", "@oxfmt/binding-linux-ppc64-gnu": "0.78.0", "@oxfmt/binding-linux-riscv64-gnu": "0.78.0", "@oxfmt/binding-linux-riscv64-musl": "0.78.0", "@oxfmt/binding-linux-s390x-gnu": "0.78.0", "@oxfmt/binding-linux-x64-gnu": "0.78.0", "@oxfmt/binding-linux-x64-musl": "0.78.0", "@oxfmt/binding-openharmony-arm64": "0.78.0", "@oxfmt/binding-win32-arm64-msvc": "0.78.0", "@oxfmt/binding-win32-ia32-msvc": "0.78.0", "@oxfmt/binding-win32-x64-msvc": "0.78.0" }, "peerDependencies": { "@astrojs/compiler-rs": "^0.5.0", "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["@astrojs/compiler-rs", "svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-uagX+70mKqh/YqxB6ON/lkxVPH6E1Z7pfP2x6XVRPPyiDCZtX6o1xIEeXAIXJ/2wA5ViX4RFuW+IAQn85yJdWg=="], + + "oxlint": ["oxlint@1.93.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.93.0", "@oxlint/binding-android-arm64": "1.93.0", "@oxlint/binding-darwin-arm64": "1.93.0", "@oxlint/binding-darwin-x64": "1.93.0", "@oxlint/binding-freebsd-x64": "1.93.0", "@oxlint/binding-linux-arm-gnueabihf": "1.93.0", "@oxlint/binding-linux-arm-musleabihf": "1.93.0", "@oxlint/binding-linux-arm64-gnu": "1.93.0", "@oxlint/binding-linux-arm64-musl": "1.93.0", "@oxlint/binding-linux-ppc64-gnu": "1.93.0", "@oxlint/binding-linux-riscv64-gnu": "1.93.0", "@oxlint/binding-linux-riscv64-musl": "1.93.0", "@oxlint/binding-linux-s390x-gnu": "1.93.0", "@oxlint/binding-linux-x64-gnu": "1.93.0", "@oxlint/binding-linux-x64-musl": "1.93.0", "@oxlint/binding-openharmony-arm64": "1.93.0", "@oxlint/binding-win32-arm64-msvc": "1.93.0", "@oxlint/binding-win32-ia32-msvc": "1.93.0", "@oxlint/binding-win32-x64-msvc": "1.93.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2005", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-19JMsQFX+YvEbcUTFQxjTwhvs0Ibg0D6CFneSLgpwnACWcY/6LjhpGpkPodh0T5aIT1pjMTLlYaeBFzaVgugTA=="], + + "oxlint-tsgolint": ["oxlint-tsgolint@7.0.2006", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "7.0.2006", "@oxlint-tsgolint/darwin-x64": "7.0.2006", "@oxlint-tsgolint/linux-arm64": "7.0.2006", "@oxlint-tsgolint/linux-x64": "7.0.2006", "@oxlint-tsgolint/win32-arm64": "7.0.2006", "@oxlint-tsgolint/win32-x64": "7.0.2006" }, "bin": { "tsgolint": "./bin/tsgolint.js" } }, "sha512-5B3UtH71kycPHUC1kFEIffivEAs49SUmkJuHZOZnTY0NPj4legp0bcsCQucuLVNCoJzDucS68P2C4WIhP+LypQ=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], @@ -262,7 +514,13 @@ "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], - "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], + + "postcss": ["postcss@8.5.32", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g=="], + + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -278,7 +536,9 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -290,9 +550,15 @@ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "tinypool": ["tinypool@2.2.0", "", {}, "sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ=="], + "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "tsrs": ["tsrs@0.24.0-ts7.1.0-beta", "", { "optionalDependencies": { "@tsrs/darwin-arm64": "0.24.0-ts7.1.0-beta", "@tsrs/darwin-x64": "0.24.0-ts7.1.0-beta", "@tsrs/linux-arm64-gnu": "0.24.0-ts7.1.0-beta", "@tsrs/linux-arm64-musl": "0.24.0-ts7.1.0-beta", "@tsrs/linux-x64-gnu": "0.24.0-ts7.1.0-beta", "@tsrs/linux-x64-musl": "0.24.0-ts7.1.0-beta", "@tsrs/win32-x64": "0.24.0-ts7.1.0-beta" }, "bin": { "tsrs": "./bin/tsrs", "tsrs-lint": "./bin/tsrs-lint" } }, "sha512-Iy7q93qfRHcI0Y31IKx6fCsDQCsSjtrT01PIgOVolBXPJTMdm84oH1pEYyAMPQ/SnRqWDSWI8wEUHbv6e9TmHA=="], + + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "undici": ["undici@7.29.1", "", {}, "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q=="], @@ -300,7 +566,9 @@ "valibot": ["valibot@1.4.2", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg=="], - "vite": ["vite@8.2.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.25", "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw=="], + "vite": ["@voidzero-dev/vite-plus-core@1.6.0", "", { "dependencies": { "@oxc-project/runtime": "=0.158.0", "@oxc-project/types": "=0.158.0", "lightningcss": "^1.33.0", "postcss": "^8.5.32", "yuku-parser": "0.19.0" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "1.6.0", "@voidzero-dev/vite-plus-darwin-x64": "1.6.0", "@voidzero-dev/vite-plus-linux-arm64-gnu": "1.6.0", "@voidzero-dev/vite-plus-linux-arm64-musl": "1.6.0", "@voidzero-dev/vite-plus-linux-x64-gnu": "1.6.0", "@voidzero-dev/vite-plus-linux-x64-musl": "1.6.0", "@voidzero-dev/vite-plus-win32-arm64-msvc": "1.6.0", "@voidzero-dev/vite-plus-win32-x64-msvc": "1.6.0", "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@types/node": "^20.19.0 || >=22.12.0", "@typescript/native-preview": "*", "@vitejs/devtools": "^0.7.1 || ^0.8.0", "@volar/typescript": "~2.4.0", "@vue/language-core": "~3.2.0 || ~3.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": ">=0.5.0", "unrun": "*", "vue-tsc": "~3.2.0 || ~3.3.0", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@types/node", "@typescript/native-preview", "@vitejs/devtools", "@volar/typescript", "@vue/language-core", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "vue-tsc", "yaml"] }, "sha512-0UFh0AVjt3ZynvRk0BTCwysCeURhyZCoEGabEptwOzlvQjV2eGRcf5jPTfLGaKwgw2dKOQYH4vq/N3H7WUdIyw=="], + + "vite-plus": ["vite-plus@1.6.0", "", { "dependencies": { "@oxc-project/runtime": "=0.158.0", "@oxc-project/types": "=0.158.0", "@oxlint/plugins": "=1.93.0", "@vitest/browser": "5.1.0", "@vitest/browser-preview": "5.1.0", "@vitest/mocker": "5.1.0", "@vitest/pretty-format": "5.1.0", "@vitest/snapshot": "5.1.0", "@vitest/spy": "5.1.0", "@vitest/utils": "5.1.0", "oxfmt": "=0.78.0", "oxlint": "=1.93.0", "oxlint-tsgolint": "=7.0.2006", "vite": "npm:@voidzero-dev/vite-plus-core@1.6.0", "vitest": "5.1.0" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "1.6.0", "@voidzero-dev/vite-plus-darwin-x64": "1.6.0", "@voidzero-dev/vite-plus-linux-arm64-gnu": "1.6.0", "@voidzero-dev/vite-plus-linux-arm64-musl": "1.6.0", "@voidzero-dev/vite-plus-linux-x64-gnu": "1.6.0", "@voidzero-dev/vite-plus-linux-x64-musl": "1.6.0", "@voidzero-dev/vite-plus-win32-arm64-msvc": "1.6.0", "@voidzero-dev/vite-plus-win32-x64-msvc": "1.6.0" }, "peerDependencies": { "@vitest/browser-playwright": "5.1.0", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "playwright": "*" }, "optionalPeers": ["@vitest/browser-playwright", "@vitest/browser-webdriverio", "playwright"], "bin": { "vp": "./bin/vp", "vpr": "./bin/vpr", "vpx": "./bin/vpx" } }, "sha512-h6aIA80QcAhICBp2Xga8lPM9jOZm2VvPn7Ls531C7RjD9herrT1rPDUMY72IdFf1XyWDmFVEwO+W3EgXtSSMLQ=="], "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], @@ -312,14 +580,102 @@ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "ws": ["ws@8.23.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-wCkmX8QMOrKjgO8HW2WyDbTSGeqkocxgDzS9iR79rEAexty6/2qzilx3Ag8Tqf9ykop3irLKle0MtVn5qjVnoQ=="], + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], - "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="], + + "yuku-core": ["yuku-core@0.19.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.19.0" }, "optionalDependencies": { "@yuku-core/binding-android-arm64": "0.19.0", "@yuku-core/binding-darwin-arm64": "0.19.0", "@yuku-core/binding-darwin-x64": "0.19.0", "@yuku-core/binding-freebsd-x64": "0.19.0", "@yuku-core/binding-linux-arm-gnu": "0.19.0", "@yuku-core/binding-linux-arm-musl": "0.19.0", "@yuku-core/binding-linux-arm64-gnu": "0.19.0", "@yuku-core/binding-linux-arm64-musl": "0.19.0", "@yuku-core/binding-linux-x64-gnu": "0.19.0", "@yuku-core/binding-linux-x64-musl": "0.19.0", "@yuku-core/binding-win32-arm64": "0.19.0", "@yuku-core/binding-win32-x64": "0.19.0" } }, "sha512-kYK9e1XdfirNhkipzW0wmMcSWcVF/CWnuugodfVYRCUwvoBefweL9HzB96OiKEhsx78V5WbUxbJqlizMX869Qg=="], + + "yuku-parser": ["yuku-parser@0.19.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.19.0", "yuku-core": "0.19.0" } }, "sha512-XrBh9ZiQK5mJVRxZBAvD6LwRyAYrK3MFtH3+onyuxFL5hFD44EC3QOmIC1FjTj6H2MMwq46/NAI+NZxLZPZgxg=="], + + "@vitest/expect/@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], + + "@vitest/expect/@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], - "@celados/argc/yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="], + "@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "@vitest/mocker/magic-string": ["magic-string@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w=="], + + "@vitest/pretty-format/tinyrainbow": ["tinyrainbow@3.2.0", "", {}, "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A=="], + + "@vitest/runner/@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + + "@vitest/snapshot/magic-string": ["magic-string@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w=="], + + "@vitest/utils/tinyrainbow": ["tinyrainbow@3.2.0", "", {}, "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A=="], "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "rolldown/@oxc-project/types": ["@oxc-project/types@0.144.0", "", {}, "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg=="], + + "vite-plus/@vitest/browser": ["@vitest/browser@5.1.0", "", { "dependencies": { "@blazediff/core": "1.10.0", "@vitest/mocker": "5.1.0", "@vitest/ui": "5.1.0", "@vitest/utils": "5.1.0", "magic-string": "^1.4.3", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.2.0", "ws": "^8.22.0" }, "peerDependencies": { "vitest": "5.1.0" } }, "sha512-2sjGVAC/Rs8Jwv+xxBgklNG2g1LGyNgcFMBw71AcfBstLz25UGv1+fVvvZ+z8IfSuNg3nipERijnFtoEIdRDEA=="], + + "vite-plus/@vitest/browser-preview": ["@vitest/browser-preview@5.1.0", "", { "dependencies": { "@testing-library/dom": "^10.4.2", "@testing-library/user-event": "^14.6.8", "@vitest/browser": "5.1.0" }, "peerDependencies": { "vitest": "5.1.0" } }, "sha512-z9SbodBSMoJtYy5VTCaSP5SSZfNYRlfIlR0P/uH1PAuhUJbstSyOYvxXCYmqG+EemIiZzd3uGUtREX5zbXLR8A=="], + + "vite-plus/vitest": ["vitest@5.1.0", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.1.0", "chai": "^6.3.0", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.4.3", "obug": "^3.0.0", "picomatch": "^4.0.7", "std-env": "^4.3.0", "tinybench": "^6.2.2", "tinyexec": "^1.3.1", "tinyglobby": "^0.2.17", "why-is-node-running": "3.2.1" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.1.0", "@vitest/browser-preview": "5.1.0", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.1.0", "@vitest/coverage-v8": "5.1.0", "@vitest/ui": "5.1.0", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA=="], + + "vitest/@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="], + + "vitest/@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + + "vitest/@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], + + "vitest/@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], + + "vitest/@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + + "vitest/vite": ["vite@8.2.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.25", "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw=="], + + "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + + "@vitest/mocker/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@vitest/runner/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + + "@vitest/snapshot/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "vite-plus/@vitest/browser/@vitest/ui": ["@vitest/ui@5.1.0", "", { "dependencies": { "@vitest/utils": "5.1.0", "fflate": "^0.8.3", "flatted": "^3.4.4", "pathe": "^2.0.3", "sirv": "^3.0.2", "tinyrainbow": "^3.2.0" }, "peerDependencies": { "vitest": "5.1.0" } }, "sha512-00S3x4NRmqIBUiTVWF9/xLaA9EP9KeKv0+1sBO7e2Y4luBISlW+2ttnhlk/zjbTqEvVhJWCCo7MXzu6zNbefqQ=="], + + "vite-plus/@vitest/browser/magic-string": ["magic-string@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w=="], + + "vite-plus/@vitest/browser/tinyrainbow": ["tinyrainbow@3.2.0", "", {}, "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A=="], + + "vite-plus/vitest/magic-string": ["magic-string@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w=="], + + "vite-plus/vitest/obug": ["obug@3.0.0", "", {}, "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw=="], + + "vite-plus/vitest/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "vite-plus/vitest/std-env": ["std-env@4.3.0", "", {}, "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag=="], + + "vite-plus/vitest/tinybench": ["tinybench@6.2.3", "", {}, "sha512-Al01C2JIBnO2CXCAPbpAl6sUiOZHeThGtHYNTa51JRw9PyWOCdr+9Ong05R4qg/Sk3+H645E8ov1h2c58ityNw=="], + + "vite-plus/vitest/tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="], + + "vite-plus/vitest/vite": ["vite@8.2.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.25", "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw=="], + + "vite-plus/vitest/why-is-node-running": ["why-is-node-running@3.2.1", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ=="], + + "vitest/vite/postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "vite-plus/@vitest/browser/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "vite-plus/vitest/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "vite-plus/vitest/vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "vite-plus/vitest/vite/postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "vitest/vite/postcss/nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "vitest/vite/postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "vite-plus/vitest/vite/postcss/nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "vite-plus/vitest/vite/postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], } } diff --git a/docs/cloud-provider-runtime.bdd.md b/docs/cloud-provider-runtime.bdd.md index d5fe0da..786770d 100644 --- a/docs/cloud-provider-runtime.bdd.md +++ b/docs/cloud-provider-runtime.bdd.md @@ -6,6 +6,7 @@ ## 范围边界 **包含:** + - 云端按业务 profile 选择 provider 配置 - 云端通过平台 secret 注入 provider 凭证 - Agent 在任何真实读取前先执行 readiness 检查 @@ -14,6 +15,7 @@ - 多业务、多环境并存时的隔离规则 **不包含:** + - secret manager 的具体供应商实现 - provider 业务指标解释、报告、归因判断 - gkit provider capability 的具体查询字段设计 @@ -35,25 +37,25 @@ **场景 1.1:任务带有明确业务 profile 时启动** Given 云端任务声明了业务 profile - And 该 profile 对应的 provider secrets 已在平台中配置 +And 该 profile 对应的 provider secrets 已在平台中配置 When Agent 启动 provider 读取流程 Then Agent 使用该业务 profile 作为本次任务的唯一业务归属 - And 后续读取只使用该 profile 绑定的 provider secrets - And Agent 不需要知道 secret 的明文内容 +And 后续读取只使用该 profile 绑定的 provider secrets +And Agent 不需要知道 secret 的明文内容 **场景 1.2:任务没有声明业务 profile 时停止** Given 云端任务没有声明业务 profile When Agent 准备读取任意真实 provider 数据 Then Agent 停止读取流程 - And 返回“缺少业务 profile”的诊断 - And 不尝试使用默认业务或本地配置兜底 +And 返回“缺少业务 profile”的诊断 +And 不尝试使用默认业务或本地配置兜底 **场景 1.3:profile 存在但没有绑定目标 provider** Given 云端任务声明了业务 profile - And 该 profile 没有绑定目标 provider 的 secret +And 该 profile 没有绑定目标 provider 的 secret When Agent 请求该 provider 的真实数据 Then Agent 返回“该业务未配置目标 provider”的诊断 - And 不从其他业务 profile 借用 secret +And 不从其他业务 profile 借用 secret --- @@ -63,43 +65,43 @@ Then Agent 返回“该业务未配置目标 provider”的诊断 **场景 2.1:通过环境变量注入 service account JSON** Given 平台 secret 中保存了目标 provider 的 service account JSON - And 云端任务声明了正确业务 profile +And 云端任务声明了正确业务 profile When 运行时启动 gkit provider command Then CLI 可以从运行时环境读取该 provider 的凭证 - And 仓库中不会出现真实凭证文件 - And 输出日志不会打印 secret 明文 +And 仓库中不会出现真实凭证文件 +And 输出日志不会打印 secret 明文 **场景 2.2:必须使用临时文件时只写入短生命周期目录** Given 某个 provider 只能通过文件路径读取凭证 - And 平台 secret 中保存了 service account JSON +And 平台 secret 中保存了 service account JSON When 运行时准备 gkit provider 输入 Then 运行时把凭证写入任务级临时目录 - And CLI 只接收该临时文件路径 - And 任务结束后该临时文件随运行环境销毁 +And CLI 只接收该临时文件路径 +And 任务结束后该临时文件随运行环境销毁 **场景 2.3:本地 `.env.live` 不参与云端运行** Given 仓库中存在本地 live validation 文档 - And 开发机可以使用 `.env.live` +And 开发机可以使用 `.env.live` When 云端任务启动 Then 云端不读取 `.env.live` - And 云端只使用平台注入的 profile 与 secrets - And 本地配置缺失不会影响云端任务 +And 云端只使用平台注入的 profile 与 secrets +And 本地配置缺失不会影响云端任务 **场景 2.4:本地开发也按业务 profile 读取凭证** Given 开发机已经创建业务 profile 目录 - And profile 目录中保存了本地只读 provider 凭证 +And profile 目录中保存了本地只读 provider 凭证 When Agent 在本地执行 gkit provider command Then CLI 按业务 profile 读取 provider 配置 - And 不要求每个 provider 维护自己的凭证目录 - And repo-local `.env.live` 不是默认配置来源 +And 不要求每个 provider 维护自己的凭证目录 +And repo-local `.env.live` 不是默认配置来源 **场景 2.5:repo 只声明 profile id,不声明 provider 凭证** Given 开发机已经创建业务 profile 目录 - And 当前 repo 的 ignored `.env.local` 只声明了业务 profile id +And 当前 repo 的 ignored `.env.local` 只声明了业务 profile id When Agent 执行 gkit provider command Then CLI 先读取 repo 中的 profile id - And 再读取该 profile 目录下的 provider 配置 - And profile 中的 provider 配置不会被 repo fallback 配置覆盖 +And 再读取该 profile 目录下的 provider 配置 +And profile 中的 provider 配置不会被 repo fallback 配置覆盖 --- @@ -109,27 +111,27 @@ Then CLI 先读取 repo 中的 profile id **场景 3.1:readiness 通过后执行低量真实读取** Given 云端任务声明了业务 profile - And 目标 provider 的必要 secrets 已注入 +And 目标 provider 的必要 secrets 已注入 When Agent 准备读取真实 provider 数据 Then Agent 先执行 readiness 检查 - And readiness 通过后才执行低量真实读取 - And 低量读取成功后才允许执行更宽的数据读取 +And readiness 通过后才执行低量真实读取 +And 低量读取成功后才允许执行更宽的数据读取 **场景 3.2:readiness 未通过时停止业务读取** Given 云端任务声明了业务 profile - And 目标 provider 的 readiness 检查未通过 +And 目标 provider 的 readiness 检查未通过 When Agent 准备读取真实 provider 数据 Then Agent 停止后续 provider 读取 - And 返回可恢复诊断 - And 不用其他 provider 的数据推断该 provider 的 truth +And 返回可恢复诊断 +And 不用其他 provider 的数据推断该 provider 的 truth **场景 3.3:可选配置缺失不阻断不需要它的读取** Given 云端任务声明了业务 profile - And 某个 provider 的可选配置未注入 - And 本次读取不需要该可选配置 +And 某个 provider 的可选配置未注入 +And 本次读取不需要该可选配置 When Agent 执行 readiness 检查 Then readiness 可以带 warning 通过 - And Agent 可以继续执行不依赖该可选配置的读取 +And Agent 可以继续执行不依赖该可选配置的读取 --- @@ -139,27 +141,27 @@ Then readiness 可以带 warning 通过 **场景 4.1:缺失必要 secret 时返回配置诊断** Given 云端任务声明了业务 profile - And 目标 provider 缺少必要 secret +And 目标 provider 缺少必要 secret When Agent 执行 readiness 检查 Then Agent 收到“必要 secret 缺失”的诊断 - And 诊断包含缺失项名称 - And 不发起真实 provider 查询 +And 诊断包含缺失项名称 +And 不发起真实 provider 查询 **场景 4.2:凭证存在但无权访问目标业务资源** Given 目标 provider 的凭证已注入 - And 该凭证无权访问当前 profile 绑定的业务资源 +And 该凭证无权访问当前 profile 绑定的业务资源 When Agent 执行低量真实读取 Then Agent 收到“provider 权限不足”的诊断 - And 诊断能与“查询结果为空”区分开 - And Agent 不把该失败解释为业务资源没有数据 +And 诊断能与“查询结果为空”区分开 +And Agent 不把该失败解释为业务资源没有数据 **场景 4.3:provider 暂时不可用时保留原始失败** Given 目标 provider 的凭证和 profile 配置都正确 - And provider 返回临时失败或限流 +And provider 返回临时失败或限流 When Agent 执行真实读取 Then Agent 返回“provider 暂不可用”的诊断 - And 标记该失败是否可以重试 - And 不用缓存、猜测或其他 provider 数据替代本次 provider truth +And 标记该失败是否可以重试 +And 不用缓存、猜测或其他 provider 数据替代本次 provider truth --- @@ -169,36 +171,36 @@ Then Agent 返回“provider 暂不可用”的诊断 **场景 5.1:两个业务 profile 同时存在时互不串用** Given 平台中同时存在多个业务 profile 的 secrets - And 云端任务声明使用其中一个 profile +And 云端任务声明使用其中一个 profile When Agent 读取 provider truth Then Agent 只使用该 profile 绑定的 secrets - And 不读取其他 profile 的 provider secrets - And 输出中标明本次使用的业务 profile +And 不读取其他 profile 的 provider secrets +And 输出中标明本次使用的业务 profile **场景 5.2:生产与测试环境同时存在时必须显式选择** Given 同一业务同时存在生产和测试 secrets When Agent 准备读取真实 provider 数据 Then Agent 必须使用任务声明的环境 - And 未声明环境时停止读取流程 - And 不默认使用生产环境 +And 未声明环境时停止读取流程 +And 不默认使用生产环境 **场景 5.3:只读 provider 读取不产生业务侧副作用** Given Agent 使用业务 profile 读取 provider truth When Agent 执行只读 gkit provider command Then provider 侧不会产生写入、变更、投放调整或账务动作 - And 本次任务只产出可审计的事实输出或诊断 +And 本次任务只产出可审计的事实输出或诊断 --- ## 配置状态真值表 -| 业务 profile | 必要 secrets | readiness | 真实读取 | Agent 行为 | -|---|---|---|---|---| -| 已声明 | 完整 | 通过 | 允许低量读取 | 先验证再扩大读取 | -| 已声明 | 缺失 | 失败 | 不允许 | 返回缺失配置诊断 | -| 未声明 | * | 不执行 | 不允许 | 返回缺少 profile 诊断 | -| 已声明 | 完整但无权限 | 可通过或读取失败 | 不允许扩大读取 | 返回权限诊断 | -| 已声明 | 完整但 provider 限流 | 失败或读取失败 | 可稍后重试 | 返回可重试诊断 | +| 业务 profile | 必要 secrets | readiness | 真实读取 | Agent 行为 | +| ------------ | -------------------- | ---------------- | -------------- | --------------------- | +| 已声明 | 完整 | 通过 | 允许低量读取 | 先验证再扩大读取 | +| 已声明 | 缺失 | 失败 | 不允许 | 返回缺失配置诊断 | +| 未声明 | * | 不执行 | 不允许 | 返回缺少 profile 诊断 | +| 已声明 | 完整但无权限 | 可通过或读取失败 | 不允许扩大读取 | 返回权限诊断 | +| 已声明 | 完整但 provider 限流 | 失败或读取失败 | 可稍后重试 | 返回可重试诊断 | ## 验收说明 diff --git a/docs/evals/clonesite-feedback-outreach-dogfood.md b/docs/evals/clonesite-feedback-outreach-dogfood.md index 5da39e9..e966619 100644 --- a/docs/evals/clonesite-feedback-outreach-dogfood.md +++ b/docs/evals/clonesite-feedback-outreach-dogfood.md @@ -6,7 +6,7 @@ description: > contacting, and learning from paid Clonesite customers. status: conditional-pass # conditional-pass | passed | failed version: 1.0 -generated: { by: codex/gpt-5, at: "2026-08-14T21:30:00+08:00" } +generated: { by: codex/gpt-5, at: '2026-08-14T21:30:00+08:00' } tags: [gkit, growth-workspace, gmail, posthog, clonesite, dogfood] --- @@ -81,15 +81,15 @@ commercial outcome. ## Acceptance gates -| Gate | Passing evidence | -| --- | --- | -| Eligibility | Product state supports the selected cohort and exclusions | -| Contact safety | No refund, suppression, active support conflict, or duplicate direct thread | -| Personalization | PostHog context is bounded, identity-grounded, and not exposed as surveillance | -| Copy | One primary question, founder voice, verified preview URL, truthful service scope | -| Authority | Draft and send permissions are explicit and separate | -| Send | Intended recipient, subject, thread, and `SENT` label read back from Gmail | -| Evaluation | Reply, bounce, complaint, later Product outcome, and service interest tracked separately | +| Gate | Passing evidence | +| --------------- | ---------------------------------------------------------------------------------------- | +| Eligibility | Product state supports the selected cohort and exclusions | +| Contact safety | No refund, suppression, active support conflict, or duplicate direct thread | +| Personalization | PostHog context is bounded, identity-grounded, and not exposed as surveillance | +| Copy | One primary question, founder voice, verified preview URL, truthful service scope | +| Authority | Draft and send permissions are explicit and separate | +| Send | Intended recipient, subject, thread, and `SENT` label read back from Gmail | +| Evaluation | Reply, bounce, complaint, later Product outcome, and service interest tracked separately | ## Product findings, not workflow facts diff --git a/docs/gkit-product-architecture-context.md b/docs/gkit-product-architecture-context.md index 1a5d290..d3942e9 100644 --- a/docs/gkit-product-architecture-context.md +++ b/docs/gkit-product-architecture-context.md @@ -4,7 +4,7 @@ title: gkit Product Architecture Context description: The confirmed minimum product boundary and first vertical slice for gkit. status: active version: 0.4 -generated: { by: codex/gpt-5, at: "2026-08-14T14:39:07+08:00" } +generated: { by: codex/gpt-5, at: '2026-08-14T14:39:07+08:00' } tags: [gkit, growth-workspace, agent-native] --- diff --git a/docs/plans/2026-07-13-gkit-provider-native-agent-surface.md b/docs/plans/2026-07-13-gkit-provider-native-agent-surface.md index 87be614..5149e45 100644 --- a/docs/plans/2026-07-13-gkit-provider-native-agent-surface.md +++ b/docs/plans/2026-07-13-gkit-provider-native-agent-surface.md @@ -103,18 +103,18 @@ gkit --profile app-a bing sites list 搜索结果必须区分 contract owner: ```ts -type CapabilityKind = "curated" | "provider"; +type CapabilityKind = 'curated' | 'provider' type CapabilitySummary = { - id: string; - provider: string; - kind: CapabilityKind; - contractOwner: "gkit" | "provider"; - description: string; - command: string; - schemaSelector: string; - effects: Effect[]; -}; + id: string + provider: string + kind: CapabilityKind + contractOwner: 'gkit' | 'provider' + description: string + command: string + schemaSelector: string + effects: Effect[] +} ``` Curated command 是 gkit 承诺的稳定、normalized contract;provider capability 明确跟随 provider 版本和原生返回。 @@ -124,31 +124,31 @@ Curated command 是 gkit 承诺的稳定、normalized contract;provider capabi 统一到 module interface 就停止: ```ts -type Effect = "read" | "spend" | "write" | "destructive"; +type Effect = 'read' | 'spend' | 'write' | 'destructive' type ProviderModule = { - id: string; - description: string; - schema: CommandTree; - handlers: HandlerTree; - profileSchema: JsonSchema; - runtime: ProviderRuntime; - catalog: { - search(query: string, limit: number): Promise; - describe(id: string): Promise; - }; - doctor(context: ProviderContext): Promise; -}; + id: string + description: string + schema: CommandTree + handlers: HandlerTree + profileSchema: JsonSchema + runtime: ProviderRuntime + catalog: { + search(query: string, limit: number): Promise + describe(id: string): Promise + } + doctor(context: ProviderContext): Promise +} type ProviderRuntime = - | { kind: "in-process"; language: "typescript" } - | { - kind: "process"; - language: "python" | "other"; - protocol: "gkit-provider-v1"; - setup: () => Promise; - probe: () => Promise; - }; + | { kind: 'in-process'; language: 'typescript' } + | { + kind: 'process' + language: 'python' | 'other' + protocol: 'gkit-provider-v1' + setup: () => Promise + probe: () => Promise + } ``` `ProviderRuntime` 只统一进程边界,不统一 provider API。DataForSEO、GSC、PostHog 可以使用 TypeScript in-process adapter;Google Ads 使用官方 Python SDK subprocess。 @@ -195,25 +195,25 @@ Core 不拥有: Profile 是真实业务维度:它回答“这次调用代表哪个 App,以及该 App 在每个 provider 中对应哪个 account/resource”。 ```ts -type SecretRef = `env:${string}`; +type SecretRef = `env:${string}` type GkitProfile = { - version: 1; - name: string; - providers: Record< - string, - { - config: Record; - secrets: Record; - } - >; -}; + version: 1 + name: string + providers: Record< + string, + { + config: Record + secrets: Record + } + > +} type ProviderContext = { - profile: string; - config: Record; - resolveSecret(name: string): string; -}; + profile: string + config: Record + resolveSecret(name: string): string +} ``` Profile 文件位于: @@ -227,37 +227,37 @@ $XDG_CONFIG_HOME/gkit/profiles/.json # XDG_CONFIG_HOME 已设置 ```json { - "version": 1, - "name": "app-a", - "providers": { - "posthog": { - "config": { - "host": "https://us.posthog.com", - "projectId": "12345" - }, - "secrets": { - "apiToken": "env:APP_A_POSTHOG_TOKEN" - } - }, - "gsc": { - "config": { - "siteUrl": "sc-domain:app-a.com" - }, - "secrets": { - "credentialsJson": "env:APP_A_GSC_CREDENTIALS_JSON" - } - }, - "google-ads": { - "config": { - "customerId": "1234567890", - "loginCustomerId": "0987654321" - }, - "secrets": { - "developerToken": "env:APP_A_GOOGLE_ADS_DEVELOPER_TOKEN", - "credentialsJson": "env:APP_A_GOOGLE_ADS_CREDENTIALS_JSON" - } - } - } + "version": 1, + "name": "app-a", + "providers": { + "posthog": { + "config": { + "host": "https://us.posthog.com", + "projectId": "12345" + }, + "secrets": { + "apiToken": "env:APP_A_POSTHOG_TOKEN" + } + }, + "gsc": { + "config": { + "siteUrl": "sc-domain:app-a.com" + }, + "secrets": { + "credentialsJson": "env:APP_A_GSC_CREDENTIALS_JSON" + } + }, + "google-ads": { + "config": { + "customerId": "1234567890", + "loginCustomerId": "0987654321" + }, + "secrets": { + "developerToken": "env:APP_A_GOOGLE_ADS_DEVELOPER_TOKEN", + "credentialsJson": "env:APP_A_GOOGLE_ADS_CREDENTIALS_JSON" + } + } + } } ``` @@ -294,13 +294,13 @@ Effect 必须由 provider module 显式标注;生成器只能提供候选, ## 3. Provider 映射 -| Provider | Discovery source | Native execution | Curated 的角色 | -|---|---|---|---| -| DataForSEO | 官方 OpenAPI | operation ID + native JSON input | 高频 SEO/GEO reads 的稳定 contract | -| GSC | Google Discovery | resource/method call | 常用 Search Analytics、Inspection 等默认值与输出 | -| Google Ads | 官方 Python SDK + field metadata | GAQL;未来 SDK service methods | 常用 performance、Keyword Planner reads | -| PostHog | instance/OpenAPI + query kinds | HogQL/query;read API | 常用 event/funnel/instrumentation reads | -| Bing Webmaster | WSDL/interface inventory | provider method + native input | 常用 site/traffic/crawl/link reads | +| Provider | Discovery source | Native execution | Curated 的角色 | +| -------------- | -------------------------------- | -------------------------------- | ------------------------------------------------ | +| DataForSEO | 官方 OpenAPI | operation ID + native JSON input | 高频 SEO/GEO reads 的稳定 contract | +| GSC | Google Discovery | resource/method call | 常用 Search Analytics、Inspection 等默认值与输出 | +| Google Ads | 官方 Python SDK + field metadata | GAQL;未来 SDK service methods | 常用 performance、Keyword Planner reads | +| PostHog | instance/OpenAPI + query kinds | HogQL/query;read API | 常用 event/funnel/instrumentation reads | +| Bing Webmaster | WSDL/interface inventory | provider method + native input | 常用 site/traffic/crawl/link reads | ### DataForSEO @@ -348,18 +348,18 @@ Discovery methods 可以生成 provider capabilities;read 与 write scope 分 ```json { - "ok": true, - "data": {}, - "meta": { - "profile": "app-a", - "provider": "dataforseo", - "capability": "dataforseo.backlinks.bulk_ranks.live", - "kind": "provider", - "effects": ["read", "spend"], - "requestId": "optional", - "cost": {"amount": 0.02, "currency": "USD"}, - "artifact": null - } + "ok": true, + "data": {}, + "meta": { + "profile": "app-a", + "provider": "dataforseo", + "capability": "dataforseo.backlinks.bulk_ranks.live", + "kind": "provider", + "effects": ["read", "spend"], + "requestId": "optional", + "cost": { "amount": 0.02, "currency": "USD" }, + "artifact": null + } } ``` @@ -367,14 +367,14 @@ Discovery methods 可以生成 provider capabilities;read 与 write scope 分 ```json { - "ok": false, - "error": { - "code": "EFFECT_NOT_ALLOWED", - "message": "This capability can spend provider credits.", - "hint": "Review the request, then rerun with --allow-spend.", - "retryable": false, - "providerCode": null - } + "ok": false, + "error": { + "code": "EFFECT_NOT_ALLOWED", + "message": "This capability can spend provider credits.", + "hint": "Review the request, then rerun with --allow-spend.", + "retryable": false, + "providerCode": null + } } ``` @@ -415,14 +415,14 @@ Provider 原始错误保留在 `details`,不伪装成统一业务错误。 ### Polyglot runtime failure modes -| Failure | Result | Required handling | -|---|---|---| -| Provider runtime 未安装 | 仅该 provider blocked | doctor 返回 setup command;root discovery 继续工作 | -| SDK/API version 不兼容 | 仅该 provider blocked | setup/doctor 校验 compatibility manifest,不尝试降级猜测 | -| Child process hang | 当前调用失败 | timeout 后终止整个 process group,返回 retryable runtime error | -| stdout 非法或过大 | 当前调用失败 | 拒绝解析并保留截断后的非敏感诊断 | -| stderr 包含敏感值 | 安全失败 | 输出前经过已解析 secret redaction,测试覆盖异常路径 | -| Python provider crash | 当前调用失败 | 非零 exit 映射为 provider runtime error,不影响其他 modules | +| Failure | Result | Required handling | +| ----------------------- | --------------------- | -------------------------------------------------------------- | +| Provider runtime 未安装 | 仅该 provider blocked | doctor 返回 setup command;root discovery 继续工作 | +| SDK/API version 不兼容 | 仅该 provider blocked | setup/doctor 校验 compatibility manifest,不尝试降级猜测 | +| Child process hang | 当前调用失败 | timeout 后终止整个 process group,返回 retryable runtime error | +| stdout 非法或过大 | 当前调用失败 | 拒绝解析并保留截断后的非敏感诊断 | +| stderr 包含敏感值 | 安全失败 | 输出前经过已解析 secret redaction,测试覆盖异常路径 | +| Python provider crash | 当前调用失败 | 非零 exit 映射为 provider runtime error,不影响其他 modules | ## 6. ADR-001: 选择一个 public binary + polyglot provider-owned execution @@ -479,11 +479,11 @@ Provider 原始错误保留在 `details`,不伪装成统一业务错误。 **Step 1: 写失败测试** ```ts -expect(run(["--schema"]).stdout).toContain("search"); -expect(run(["--schema"]).stdout).toContain("describe"); -expect(run(["--schema"]).stdout).toContain("profile"); -expect(run(["--schema"]).stdout).toContain("dataforseo"); -expect(run(["--schema=.dataforseo"]).stdout).not.toContain("posthog"); +expect(run(['--schema']).stdout).toContain('search') +expect(run(['--schema']).stdout).toContain('describe') +expect(run(['--schema']).stdout).toContain('profile') +expect(run(['--schema']).stdout).toContain('dataforseo') +expect(run(['--schema=.dataforseo']).stdout).not.toContain('posthog') ``` **Step 2: 建立 root contract** @@ -492,8 +492,8 @@ Root package identity 固定为: ```json { - "name": "@deniffer/gkit", - "bin": {"gkit": "bin/gkit.js"} + "name": "@deniffer/gkit", + "bin": { "gkit": "bin/gkit.js" } } ``` @@ -553,9 +553,9 @@ git commit -m "feat: establish single gkit runtime" **Tests:** ```ts -expect(search("campaign performance")[0].kind).toBe("curated"); -expect(describe(id).command).toMatch(/^gkit google-ads /); -expect(() => describe("missing")).toThrowError("CAPABILITY_NOT_FOUND"); +expect(search('campaign performance')[0].kind).toBe('curated') +expect(describe(id).command).toMatch(/^gkit google-ads /) +expect(() => describe('missing')).toThrowError('CAPABILITY_NOT_FOUND') ``` ```bash @@ -663,15 +663,15 @@ git commit -m "feat: enforce gkit execution policy" ```ts type ProviderProcessRequest = { - version: 1; - command: string; - input: unknown; - context: { profile: string; config: Record }; -}; + version: 1 + command: string + input: unknown + context: { profile: string; config: Record } +} type ProviderProcessResponse = - | { ok: true; data: unknown; meta?: Record } - | { ok: false; error: ProviderError }; + | { ok: true; data: unknown; meta?: Record } + | { ok: false; error: ProviderError } ``` Secret value 不属于 request type。Provider-owned environment mapper 从 `ProviderContext.resolveSecret()` 构造 allowlisted child env;不继承完整 parent environment。 diff --git a/docs/plans/2026-07-13-gkit-vertical-slice-plan.md b/docs/plans/2026-07-13-gkit-vertical-slice-plan.md index b9794da..07b0f8c 100644 --- a/docs/plans/2026-07-13-gkit-vertical-slice-plan.md +++ b/docs/plans/2026-07-13-gkit-vertical-slice-plan.md @@ -58,35 +58,35 @@ Profile 文件:`$XDG_CONFIG_HOME/gkit/profiles/.json`(未设置时 `~/.con ```json { - "version": 1, - "name": "app-a", - "providers": { - "dataforseo": { - "config": {}, - "policy": { "maxSpendUsdPerCall": "0.50" }, - "secrets": { - "login": "env:APP_A_DATAFORSEO_LOGIN", - "password": "env:APP_A_DATAFORSEO_PASSWORD" - } - }, - "posthog": { - "config": { "host": "https://us.posthog.com", "projectId": "12345" }, - "secrets": { "apiToken": "env:APP_A_POSTHOG_TOKEN" } - }, - "gsc": { - "config": { "siteUrl": "sc-domain:app-a.com" }, - "secrets": { "credentialsJson": "env:APP_A_GSC_CREDENTIALS_JSON" } - }, - "google-ads": { - "config": { - "customerId": "1234567890" - }, - "secrets": { - "developerToken": "env:APP_A_GOOGLE_ADS_DEVELOPER_TOKEN", - "serviceAccountFile": "env:APP_A_GOOGLE_ADS_SERVICE_ACCOUNT_FILE" - } - } - } + "version": 1, + "name": "app-a", + "providers": { + "dataforseo": { + "config": {}, + "policy": { "maxSpendUsdPerCall": "0.50" }, + "secrets": { + "login": "env:APP_A_DATAFORSEO_LOGIN", + "password": "env:APP_A_DATAFORSEO_PASSWORD" + } + }, + "posthog": { + "config": { "host": "https://us.posthog.com", "projectId": "12345" }, + "secrets": { "apiToken": "env:APP_A_POSTHOG_TOKEN" } + }, + "gsc": { + "config": { "siteUrl": "sc-domain:app-a.com" }, + "secrets": { "credentialsJson": "env:APP_A_GSC_CREDENTIALS_JSON" } + }, + "google-ads": { + "config": { + "customerId": "1234567890" + }, + "secrets": { + "developerToken": "env:APP_A_GOOGLE_ADS_DEVELOPER_TOKEN", + "serviceAccountFile": "env:APP_A_GOOGLE_ADS_SERVICE_ACCOUNT_FILE" + } + } + } } ``` @@ -117,19 +117,19 @@ Effect 不可按 HTTP method 推断(GSC Search Analytics 是 POST+read;DataForSE ```json { - "ok": true, - "data": {}, - "meta": { - "profile": "app-a", - "provider": "dataforseo", - "capability": "dataforseo.backlinks.bulk_ranks.live", - "effects": ["read", "spend"], - "cost": { "amount": "0.020000", "currency": "USD" }, - "artifact": null, - "attemptId": "01J...", - "spendOutcome": "confirmed_charged", - "providerRequestId": "provider-request-id" - } + "ok": true, + "data": {}, + "meta": { + "profile": "app-a", + "provider": "dataforseo", + "capability": "dataforseo.backlinks.bulk_ranks.live", + "effects": ["read", "spend"], + "cost": { "amount": "0.020000", "currency": "USD" }, + "artifact": null, + "attemptId": "01J...", + "spendOutcome": "confirmed_charged", + "providerRequestId": "provider-request-id" + } } ``` @@ -137,15 +137,15 @@ Effect 不可按 HTTP method 推断(GSC Search Analytics 是 POST+read;DataForSE ```json { - "ok": false, - "error": { - "code": "EFFECT_NOT_ALLOWED", - "message": "This capability can spend provider credits.", - "hint": "Review the request, then rerun with --allow-spend.", - "retryable": false, - "outcome": "not_dispatched", - "details": null - } + "ok": false, + "error": { + "code": "EFFECT_NOT_ALLOWED", + "message": "This capability can spend provider credits.", + "hint": "Review the request, then rerun with --allow-spend.", + "retryable": false, + "outcome": "not_dispatched", + "details": null + } } ``` @@ -241,13 +241,13 @@ gkit docs [--provider

] # 打印文档目录路径,agent 自行 r ## 4. Provider 映射(全部 in-process TypeScript) -| Provider | Contract 来源 | 执行面 | 认证 | -| -------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -| DataForSEO | 官方 OpenAPI(pinned) | `api call --operation-id --input @req.json` | basic auth(login/password) | -| GSC | Google Discovery | `search-analytics query` 等 resource/method | service account JSON | -| PostHog | Query API + OpenAPI reads | `query hogql --input @query.json` | personal API token | +| Provider | Contract 来源 | 执行面 | 认证 | +| -------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| DataForSEO | 官方 OpenAPI(pinned) | `api call --operation-id --input @req.json` | basic auth(login/password) | +| GSC | Google Discovery | `search-analytics query` 等 resource/method | service account JSON | +| PostHog | Query API + OpenAPI reads | `query hogql --input @query.json` | personal API token | | Google Ads | 官方 REST reference(`googleAds:search`、`googleAdsFields:search`、Keyword Plan services),固定 API major version | `query gaql`(默认显式分页)、`fields search/describe`、经保留决策的 Keyword Planner read | service-account JSON(当前 profile)或 OAuth2 refresh token + developer token;access token 作为派生 secret | -| Bing Webmaster | 现有 JSON REST API contract,WSDL 仅在 REST 不覆盖时考虑 | provider method + native input | 现有 API key 走 `apikey` query parameter(完整 URL 永不打印);未来采用 OAuth 时才用 Bearer | +| Bing Webmaster | 现有 JSON REST API contract,WSDL 仅在 REST 不覆盖时考虑 | provider method + native input | 现有 API key 走 `apikey` query parameter(完整 URL 永不打印);未来采用 OAuth 时才用 Bearer | 关于 Google Ads 的决策:v1 的 bounded read surface 走 REST;默认使用可控的 `googleAds:search` 分页,不以 `searchStream` 一次吞入任意大结果。Slice 1.5 已用真实账号覆盖 service-account OAuth、field metadata、request ID/error projection、分页和当前 Keyword Planner 用法;真实 profile 没有 MCC,因此 manager account header 仍是暴露该能力前的独立 live gate。当前唯一消费者的单账户 surface 不被这个缺口阻断。未来若开放 mutate、REST 无法覆盖必需能力或引入不可信依赖,再设计 polyglot/isolated runtime。 @@ -342,29 +342,29 @@ Raw dogfood event 已追加到 `$XDG_STATE_HOME/gkit/dogfood/events.jsonl`(未 ```json { - "ts": "", - "windowId": "slice1-2026-07-14", - "taskId": "", - "kind": "explicit_provider|business_goal|negative", - "eligible": true, - "route": "gkit|legacy_keep|blocked|break_glass", - "profile": "clonesite.ai|null", - "capability": "", - "targetCount": 1, - "discoverySteps": 0, - "firstExecutableCommandCorrect": true, - "paidDispatch": false, - "providerNetworkCalls": 0, - "attemptId": null, - "artifactBytes": null, - "artifactSha256": null, - "externalMaxCostMicros": null, - "externalActualCostMicros": null, - "externalEvidenceRef": null, - "result": "success|safe_block|failure", - "artifactUsedDownstream": false, - "fallbackReason": null, - "friction": null + "ts": "", + "windowId": "slice1-2026-07-14", + "taskId": "", + "kind": "explicit_provider|business_goal|negative", + "eligible": true, + "route": "gkit|legacy_keep|blocked|break_glass", + "profile": "clonesite.ai|null", + "capability": "", + "targetCount": 1, + "discoverySteps": 0, + "firstExecutableCommandCorrect": true, + "paidDispatch": false, + "providerNetworkCalls": 0, + "attemptId": null, + "artifactBytes": null, + "artifactSha256": null, + "externalMaxCostMicros": null, + "externalActualCostMicros": null, + "externalEvidenceRef": null, + "result": "success|safe_block|failure", + "artifactUsedDownstream": false, + "fallbackReason": null, + "friction": null } ``` diff --git a/package.json b/package.json index de9ce5e..2408db7 100644 --- a/package.json +++ b/package.json @@ -1,40 +1,39 @@ { - "name": "gkit", - "private": true, - "workspaces": { - "packages": [ - "packages/gkit" - ], - "catalog": { - "@types/bun": "^1.3.10", - "@types/node": "^25.6.0", - "@valibot/to-json-schema": "^1.6.0", - "ajv": "^8.20.0", - "@celados/argc": "^11.0.0", - "bun-types": "^1.3.13", - "dataforseo-client": "^2.0.25", - "dotenv": "^17.2.2", - "google-auth-library": "^10.9.0", - "typescript": "^5", - "valibot": "^1.2.0", - "vitest": "^4.0.18", - "yaml": "^2.9.0" - } - }, - "type": "module", - "scripts": { - "cli": "bun run --filter gkit cli", - "eval": "bun run --filter gkit eval:slice5", - "check-types": "bun run --filter gkit check-types", - "test": "bun run --filter gkit test:once", - "verify:package": "bun run ./packages/gkit/scripts/verify-package-artifact.ts" - }, - "devDependencies": { - "@types/bun": "catalog:", - "@types/node": "catalog:", - "bun-types": "catalog:", - "typescript": "catalog:", - "vitest": "catalog:" - }, - "packageManager": "bun@1.3.10" + "name": "gkit", + "private": true, + "workspaces": { + "packages": [ + "packages/gkit" + ], + "catalog": { + "@types/bun": "^1.3.10", + "@types/node": "^25.6.0", + "bun-types": "^1.3.13", + "typescript": "^7.0.2", + "vitest": "^4.0.18" + } + }, + "type": "module", + "scripts": { + "cli": "bun run --filter gkit cli", + "eval": "bun run --filter gkit eval:slice5", + "typecheck": "harness typecheck", + "check": "vp check && harness typecheck && fallow dead-code && bun run generate:check && bun run test", + "generate:check": "bun run --filter gkit generate:check", + "test": "bun run --filter gkit test:once", + "verify:package": "bun run ./packages/gkit/scripts/verify-package-artifact.ts", + "prepare": "vp config" + }, + "devDependencies": { + "@celados/harness": "^0.1.3", + "@types/bun": "catalog:", + "@types/node": "catalog:", + "bun-types": "catalog:", + "fallow": "^3.42.0", + "tsrs": "0.24.0-ts7.1.0-beta", + "typescript": "catalog:", + "vite-plus": "^1.6.0", + "vitest": "catalog:" + }, + "packageManager": "bun@1.3.10" } diff --git a/packages/gkit/bin/gkit.js b/packages/gkit/bin/gkit.js index e10dbb2..bb95b41 100755 --- a/packages/gkit/bin/gkit.js +++ b/packages/gkit/bin/gkit.js @@ -1,4 +1,4 @@ #!/usr/bin/env bun -import { main } from "../src/cli.ts"; +import { main } from '../src/cli.ts' -await main(); +await main() diff --git a/packages/gkit/docs/site.md b/packages/gkit/docs/site.md index 467ea76..f142b2d 100644 --- a/packages/gkit/docs/site.md +++ b/packages/gkit/docs/site.md @@ -73,16 +73,23 @@ Site configuration defaults to `//site.json`; t ```json { - "version": 1, - "site": "example.com", - "origins": ["https://example.com"], - "seeds": ["https://example.com/", "https://example.com/fr/"], - "locales": ["en", "fr"], - "github": { "repository": "owner/site" }, - "timeoutMs": 15000, - "delayMs": 200, - "maxBytes": 5000000, - "rules": [{ "pathPrefix": "/", "indexable": true, "canonical": "self", "required": ["title", "description", "h1"] }] + "version": 1, + "site": "example.com", + "origins": ["https://example.com"], + "seeds": ["https://example.com/", "https://example.com/fr/"], + "locales": ["en", "fr"], + "github": { "repository": "owner/site" }, + "timeoutMs": 15000, + "delayMs": 200, + "maxBytes": 5000000, + "rules": [ + { + "pathPrefix": "/", + "indexable": true, + "canonical": "self", + "required": ["title", "description", "h1"] + } + ] } ``` @@ -143,4 +150,4 @@ The earlier unshipped `sync`, `releases`, `findings`, and `export` ledger comman # Validation -From the repository root: `bun run check-types`, `bun run test`, and `bun run verify:package`. The package check installs the tarball in an isolated directory and loads discovery with an isolated profile, without network capture. Current validation includes 190 tests and 15 CLI scenarios covering static/SSR capture, relocation, offline audit/diff, coverage, corruption, output reservation, exact Git input, and disposable caching. Real Nuxt 4.4.2 build evidence lives in the product workspace, outside this generic package. +From the repository root: `bun run check` and `bun run verify:package`. The package check installs the tarball in an isolated directory and loads discovery with an isolated profile, without network capture. Current validation includes 190 tests and 15 CLI scenarios covering static/SSR capture, relocation, offline audit/diff, coverage, corruption, output reservation, exact Git input, and disposable caching. Real Nuxt 4.4.2 build evidence lives in the product workspace, outside this generic package. diff --git a/packages/gkit/evals/baseline.md b/packages/gkit/evals/baseline.md index 47ca8ef..138c757 100644 --- a/packages/gkit/evals/baseline.md +++ b/packages/gkit/evals/baseline.md @@ -20,12 +20,12 @@ negative tasks made no provider calls and added no ledger attempts. ## Results -| Task | Kind | Discovery | Provider calls | Result | Cost | -| --- | --- | ---: | ---: | --- | ---: | -| `no_spend_negative` | negative | 1 step | 0 | discovery only; safely blocked | `$0` | -| `unsupported_mutation_negative` | negative | 1 step | 0 | exit 1, `CAPABILITY_NOT_FOUND`, `not_dispatched` | `$0` | -| `dfs_bulk_ranks_explicit` | explicit provider | 1 step | 1 | first live attempt succeeded | `$0.024072` | -| `compare_domain_authority_goal` | business goal | 2 steps | 1 | first live attempt succeeded | `$0.024108` | +| Task | Kind | Discovery | Provider calls | Result | Cost | +| ------------------------------- | ----------------- | --------: | -------------: | ------------------------------------------------ | ----------: | +| `no_spend_negative` | negative | 1 step | 0 | discovery only; safely blocked | `$0` | +| `unsupported_mutation_negative` | negative | 1 step | 0 | exit 1, `CAPABILITY_NOT_FOUND`, `not_dispatched` | `$0` | +| `dfs_bulk_ranks_explicit` | explicit provider | 1 step | 1 | first live attempt succeeded | `$0.024072` | +| `compare_domain_authority_goal` | business goal | 2 steps | 1 | first live attempt succeeded | `$0.024108` | Authorized exposure and actual cost were both `$0.048180`, below the `$0.15` gate cap. diff --git a/packages/gkit/evals/bing-migration-matrix.md b/packages/gkit/evals/bing-migration-matrix.md index 5fd9fad..3366a68 100644 --- a/packages/gkit/evals/bing-migration-matrix.md +++ b/packages/gkit/evals/bing-migration-matrix.md @@ -26,26 +26,26 @@ doctor. ## `@deniffer/bing-webmaster-cli` -| Legacy command | Decision | gkit target or missing gate | -| --- | --- | --- | -| `doctor dataset readiness` | keep | `gkit bing doctor` validates local config but intentionally does not preserve the legacy network probe | -| `site dataset sites` | replace | `bing.sites.list`; both returned the same five sites | -| `traffic dataset rank` | replace | `bing.traffic.rank`; same seven rows | -| `traffic dataset queries` | replace | `bing.traffic.queries`; same 35 rows, with provider order varying between requests | -| `traffic dataset pages` | replace | `bing.traffic.pages`; same 28 rows | -| `traffic entity query` | replace | `bing.traffic.query`; same six rows for the selected query | -| `traffic entity pageQueries` | replace | `bing.traffic.page-queries`; same 26 rows, with provider order varying between requests | -| `traffic entity queryPages` | replace | `bing.traffic.query-pages`; same three rows | -| `traffic entity queryPage` | replace | `bing.traffic.query-page`; both returned the same valid empty result | -| `crawl dataset stats` | replace | `bing.crawl.stats`; same six rows | -| `crawl dataset issues` | replace | `bing.crawl.issues`; both returned the same valid empty result | -| `crawl entity settings` | replace | `bing.crawl.settings`; same settings object | -| `link dataset pages` | replace | `bing.links.pages`; same link-count object | -| `link entity url` | replace | `bing.links.url`; same URL-link object | -| `sitemap dataset feeds` | replace | `bing.sitemaps.list`; same one-sitemap result | -| `sitemap entity feed` | replace | `bing.sitemaps.get`; same four detail rows | -| `url entity info` | replace | `bing.urls.info`; same URL index object | -| `url entity traffic` | replace | `bing.urls.traffic`; same URL traffic object | +| Legacy command | Decision | gkit target or missing gate | +| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------ | +| `doctor dataset readiness` | keep | `gkit bing doctor` validates local config but intentionally does not preserve the legacy network probe | +| `site dataset sites` | replace | `bing.sites.list`; both returned the same five sites | +| `traffic dataset rank` | replace | `bing.traffic.rank`; same seven rows | +| `traffic dataset queries` | replace | `bing.traffic.queries`; same 35 rows, with provider order varying between requests | +| `traffic dataset pages` | replace | `bing.traffic.pages`; same 28 rows | +| `traffic entity query` | replace | `bing.traffic.query`; same six rows for the selected query | +| `traffic entity pageQueries` | replace | `bing.traffic.page-queries`; same 26 rows, with provider order varying between requests | +| `traffic entity queryPages` | replace | `bing.traffic.query-pages`; same three rows | +| `traffic entity queryPage` | replace | `bing.traffic.query-page`; both returned the same valid empty result | +| `crawl dataset stats` | replace | `bing.crawl.stats`; same six rows | +| `crawl dataset issues` | replace | `bing.crawl.issues`; both returned the same valid empty result | +| `crawl entity settings` | replace | `bing.crawl.settings`; same settings object | +| `link dataset pages` | replace | `bing.links.pages`; same link-count object | +| `link entity url` | replace | `bing.links.url`; same URL-link object | +| `sitemap dataset feeds` | replace | `bing.sitemaps.list`; same one-sitemap result | +| `sitemap entity feed` | replace | `bing.sitemaps.get`; same four detail rows | +| `url entity info` | replace | `bing.urls.info`; same URL index object | +| `url entity traffic` | replace | `bing.urls.traffic`; same URL traffic object | ## Package retirement verdict diff --git a/packages/gkit/evals/dataforseo-migration-matrix.md b/packages/gkit/evals/dataforseo-migration-matrix.md index fe042ef..5c6aad1 100644 --- a/packages/gkit/evals/dataforseo-migration-matrix.md +++ b/packages/gkit/evals/dataforseo-migration-matrix.md @@ -25,43 +25,43 @@ missing. No command is dropped in Slice 2. ## `@deniffer/backlink-cli` -| Legacy command | Decision | gkit target or missing gate | -| --- | --- | --- | -| `doctor dataset readiness` | replace | `gkit --profile dataforseo doctor` | -| `domain dataset summary` | replace | `dataforseo.backlinks.summary.live`; domain live gate passed | -| `domain dataset referringDomains` | replace | `dataforseo.backlinks.referring_domains.live`; limit/order live gate passed | -| `domain dataset anchors` | keep | no reviewed executable manifest record | -| `page dataset summary` | keep | endpoint is exposed, but page-target behavior has no dedicated golden/live gate | -| `page dataset backlinks` | keep | no reviewed executable manifest record | -| `page dataset anchors` | keep | no reviewed executable manifest record | +| Legacy command | Decision | gkit target or missing gate | +| --------------------------------- | -------- | ------------------------------------------------------------------------------- | +| `doctor dataset readiness` | replace | `gkit --profile dataforseo doctor` | +| `domain dataset summary` | replace | `dataforseo.backlinks.summary.live`; domain live gate passed | +| `domain dataset referringDomains` | replace | `dataforseo.backlinks.referring_domains.live`; limit/order live gate passed | +| `domain dataset anchors` | keep | no reviewed executable manifest record | +| `page dataset summary` | keep | endpoint is exposed, but page-target behavior has no dedicated golden/live gate | +| `page dataset backlinks` | keep | no reviewed executable manifest record | +| `page dataset anchors` | keep | no reviewed executable manifest record | The package stays because three command families remain `keep` and page summary has not passed its own behavior gate. ## `@deniffer/serp-snapshot-cli` -| Legacy command | Decision | gkit target or missing gate | -| --- | --- | --- | -| `doctor dataset readiness` | replace | `gkit --profile dataforseo doctor` | -| `query dataset results` | replace | `dataforseo.serp.google.organic.live.advanced`; US/en/desktop/windows/depth-10 live gate passed | -| `batch dataset results` | keep | gkit exposes one reviewed provider task per invocation; batch artifact behavior is not replaced | +| Legacy command | Decision | gkit target or missing gate | +| -------------------------- | -------- | ----------------------------------------------------------------------------------------------- | +| `doctor dataset readiness` | replace | `gkit --profile dataforseo doctor` | +| `query dataset results` | replace | `dataforseo.serp.google.organic.live.advanced`; US/en/desktop/windows/depth-10 live gate passed | +| `batch dataset results` | keep | gkit exposes one reviewed provider task per invocation; batch artifact behavior is not replaced | The package stays because batch behavior is still `keep`. ## `@deniffer/ai-optimization-cli` -| Legacy command | Decision | gkit target or missing gate | -| --- | --- | --- | -| `doctor dataset readiness` | replace | `gkit --profile dataforseo doctor` | -| `llmResponse dataset models` | keep | no reviewed executable manifest record | -| `llmResponse entity live` | keep | no reviewed executable manifest record or live gate | -| `llmMention dataset locationsAndLanguages` | keep | no reviewed executable manifest record | -| `llmMention dataset availableFilters` | keep | no reviewed executable manifest record | -| `llmMention dataset search` | keep | inventory-only; `$0.10` request floor exceeds the `$0.03` profile cap | -| `llmMention dataset topPages` | keep | no reviewed executable manifest record | -| `llmMention dataset topDomains` | keep | no reviewed executable manifest record | -| `llmMention dataset aggregatedMetrics` | keep | no reviewed executable manifest record | -| `llmMention dataset crossAggregatedMetrics` | keep | no reviewed executable manifest record | +| Legacy command | Decision | gkit target or missing gate | +| ------------------------------------------- | -------- | --------------------------------------------------------------------- | +| `doctor dataset readiness` | replace | `gkit --profile dataforseo doctor` | +| `llmResponse dataset models` | keep | no reviewed executable manifest record | +| `llmResponse entity live` | keep | no reviewed executable manifest record or live gate | +| `llmMention dataset locationsAndLanguages` | keep | no reviewed executable manifest record | +| `llmMention dataset availableFilters` | keep | no reviewed executable manifest record | +| `llmMention dataset search` | keep | inventory-only; `$0.10` request floor exceeds the `$0.03` profile cap | +| `llmMention dataset topPages` | keep | no reviewed executable manifest record | +| `llmMention dataset topDomains` | keep | no reviewed executable manifest record | +| `llmMention dataset aggregatedMetrics` | keep | no reviewed executable manifest record | +| `llmMention dataset crossAggregatedMetrics` | keep | no reviewed executable manifest record | The package stays because every data workflow remains `keep`. diff --git a/packages/gkit/evals/google-ads-migration-matrix.md b/packages/gkit/evals/google-ads-migration-matrix.md index 560de14..6464e55 100644 --- a/packages/gkit/evals/google-ads-migration-matrix.md +++ b/packages/gkit/evals/google-ads-migration-matrix.md @@ -26,18 +26,18 @@ legacy runtime-only operation in the REST product. ## `@deniffer/google-ads-cli` -| Legacy command | Decision | gkit target or missing gate | -| --- | --- | --- | -| `provider action install` | drop | REST is in-process and has no package-local Python environment to install | -| `doctor dataset readiness` | keep | gkit now validates the local single-account service-account profile, but it intentionally does not preserve legacy network/MCC readiness semantics | -| `customer dataset accounts` | replace | `google-ads.customers.list-accessible`; implemented contract and live artifact passed | -| `query dataset gaql` | replace | `google-ads.query.gaql`; pinned search, explicit pagination, raw page bundle, negative error, and live gate passed | -| `keywordPlan dataset ideas` | replace | `google-ads.keyword-plan.generate-ideas`; provider-native input, full-page live result, artifact, and prior legacy comparison passed | -| `keywordPlan dataset historicalMetrics` | replace | `google-ads.keyword-plan.generate-historical-metrics`; live REST and legacy comparison passed | -| `campaign dataset performance` | keep | generic GAQL is available, but the curated command's exact query/input/output golden has not run through gkit | -| `adGroup dataset performance` | keep | generic GAQL is available, but the curated command's exact query/input/output golden has not run through gkit | -| `keyword dataset performance` | keep | generic GAQL is available, but the curated command's exact query/input/output golden has not run through gkit | -| `searchTerm dataset performance` | keep | the larger REST query proved capacity, not the curated command's complete behavior contract | +| Legacy command | Decision | gkit target or missing gate | +| --------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `provider action install` | drop | REST is in-process and has no package-local Python environment to install | +| `doctor dataset readiness` | keep | gkit now validates the local single-account service-account profile, but it intentionally does not preserve legacy network/MCC readiness semantics | +| `customer dataset accounts` | replace | `google-ads.customers.list-accessible`; implemented contract and live artifact passed | +| `query dataset gaql` | replace | `google-ads.query.gaql`; pinned search, explicit pagination, raw page bundle, negative error, and live gate passed | +| `keywordPlan dataset ideas` | replace | `google-ads.keyword-plan.generate-ideas`; provider-native input, full-page live result, artifact, and prior legacy comparison passed | +| `keywordPlan dataset historicalMetrics` | replace | `google-ads.keyword-plan.generate-historical-metrics`; live REST and legacy comparison passed | +| `campaign dataset performance` | keep | generic GAQL is available, but the curated command's exact query/input/output golden has not run through gkit | +| `adGroup dataset performance` | keep | generic GAQL is available, but the curated command's exact query/input/output golden has not run through gkit | +| `keyword dataset performance` | keep | generic GAQL is available, but the curated command's exact query/input/output golden has not run through gkit | +| `searchTerm dataset performance` | keep | the larger REST query proved capacity, not the curated command's complete behavior contract | ## Package retirement verdict diff --git a/packages/gkit/evals/google-ads-rest-spike.md b/packages/gkit/evals/google-ads-rest-spike.md index b205950..b96ddd9 100644 --- a/packages/gkit/evals/google-ads-rest-spike.md +++ b/packages/gkit/evals/google-ads-rest-spike.md @@ -45,16 +45,16 @@ not evidence that the manager scenario passed. The isolated spike made ten REST calls: nine expected `200` responses and one expected `400` negative response. -| Surface | Observed result | -| --- | --- | -| OAuth | service-account credentials produced a derived access token; the token was registered as a secret immediately | -| Accessible customers | one directly accessible account; zero directly accessible manager accounts | -| Field metadata | two pages and two rows with a real `pageToken` loop | -| Small GAQL | one page and 12 campaign rows; legacy and REST row counts matched | -| Larger GAQL | one page and 850 search-term rows; peak observed heap delta was 1,266,192 bytes | -| Keyword Ideas | two pages and four rows using a real `pageToken`; legacy and REST counts matched | -| Historical metrics | one page and one row; legacy and REST counts matched | -| Negative GAQL | HTTP `400`, status `INVALID_ARGUMENT`, allowlisted code `queryError:UNRECOGNIZED_FIELD`, and a request ID | +| Surface | Observed result | +| -------------------- | ------------------------------------------------------------------------------------------------------------- | +| OAuth | service-account credentials produced a derived access token; the token was registered as a secret immediately | +| Accessible customers | one directly accessible account; zero directly accessible manager accounts | +| Field metadata | two pages and two rows with a real `pageToken` loop | +| Small GAQL | one page and 12 campaign rows; legacy and REST row counts matched | +| Larger GAQL | one page and 850 search-term rows; peak observed heap delta was 1,266,192 bytes | +| Keyword Ideas | two pages and four rows using a real `pageToken`; legacy and REST counts matched | +| Historical metrics | one page and one row; legacy and REST counts matched | +| Negative GAQL | HTTP `400`, status `INVALID_ARGUMENT`, allowlisted code `queryError:UNRECOGNIZED_FIELD`, and a request ID | REST response fields are provider-native camelCase. The legacy Python wrapper converted protobuf messages to snake_case; gkit should preserve the REST diff --git a/packages/gkit/evals/gsc-migration-matrix.md b/packages/gkit/evals/gsc-migration-matrix.md index de3796a..e6cdefa 100644 --- a/packages/gkit/evals/gsc-migration-matrix.md +++ b/packages/gkit/evals/gsc-migration-matrix.md @@ -18,17 +18,17 @@ resource: ../../../docs/plans/2026-07-13-gkit-vertical-slice-plan.md ## `@deniffer/gsc-cli` -| Legacy command | Decision | gkit target or evidence | -| --- | --- | --- | -| `skill path` | drop | local package skill distribution is not a provider capability; generated docs replace discovery | -| `skill print` | drop | `gkit docs --provider gsc` and `gkit describe` are the canonical discovery surfaces | -| `skill install` | drop | the sole consumer uses the linked `gkit` binary; no package-local skill install is needed | -| `doctor dataset readiness` | keep | `gkit gsc doctor` validates local files, but intentionally does not preserve the legacy network probe | -| `property dataset sites` | replace | `gsc.properties.list`; legacy and gkit both returned two properties | -| `search dataset analytics` | replace | `gsc.search-analytics.query`; same dates, query dimension, and limit returned the same 25 rows | -| `sitemap dataset sitemaps` | replace | `gsc.sitemaps.list`; both returned one sitemap with the same raw provider facts | -| `sitemap entity sitemap` | replace | `gsc.sitemaps.get`; both returned the selected sitemap object | -| `inspection entity url` | replace | `gsc.url-inspection.inspect`; both returned the same indexed URL result | +| Legacy command | Decision | gkit target or evidence | +| -------------------------- | -------- | ----------------------------------------------------------------------------------------------------- | +| `skill path` | drop | local package skill distribution is not a provider capability; generated docs replace discovery | +| `skill print` | drop | `gkit docs --provider gsc` and `gkit describe` are the canonical discovery surfaces | +| `skill install` | drop | the sole consumer uses the linked `gkit` binary; no package-local skill install is needed | +| `doctor dataset readiness` | keep | `gkit gsc doctor` validates local files, but intentionally does not preserve the legacy network probe | +| `property dataset sites` | replace | `gsc.properties.list`; legacy and gkit both returned two properties | +| `search dataset analytics` | replace | `gsc.search-analytics.query`; same dates, query dimension, and limit returned the same 25 rows | +| `sitemap dataset sitemaps` | replace | `gsc.sitemaps.list`; both returned one sitemap with the same raw provider facts | +| `sitemap entity sitemap` | replace | `gsc.sitemaps.get`; both returned the selected sitemap object | +| `inspection entity url` | replace | `gsc.url-inspection.inspect`; both returned the same indexed URL result | ## Package retirement verdict diff --git a/packages/gkit/evals/slice2-baseline.md b/packages/gkit/evals/slice2-baseline.md index 7e0e007..0995148 100644 --- a/packages/gkit/evals/slice2-baseline.md +++ b/packages/gkit/evals/slice2-baseline.md @@ -35,11 +35,11 @@ the active profile hard cap of `$0.03` per call. ## Live gates -| Capability | Requested bound | Actual cost | Provider result | Artifact | -| --- | ---: | ---: | --- | --- | -| `dataforseo.backlinks.summary.live` | `$0.024036` | `$0.024036` | one summary result | 2,846 bytes, SHA-256 `061c05629f2a5857e52899caf9c96c8b029598990274116a4ae4bd4253564463` | -| `dataforseo.backlinks.referring_domains.live` | `$0.024072` | `$0.024036` | requested limit 2, returned 1 row | 2,921 bytes, SHA-256 `d1e83a6f24b9bfdb4f36b6398ecdbe960a58c45337e47c5f0dc4f451d19d223b` | -| `dataforseo.serp.google.organic.live.advanced` | `$0.002000` | `$0.002000` | one page, 11 SERP elements | 25,043 bytes, SHA-256 `f557cc9f963a888d47f8d4ac333fef873ed71f955290f90f9f06227aba26e6cb` | +| Capability | Requested bound | Actual cost | Provider result | Artifact | +| ---------------------------------------------- | --------------: | ----------: | --------------------------------- | ---------------------------------------------------------------------------------------- | +| `dataforseo.backlinks.summary.live` | `$0.024036` | `$0.024036` | one summary result | 2,846 bytes, SHA-256 `061c05629f2a5857e52899caf9c96c8b029598990274116a4ae4bd4253564463` | +| `dataforseo.backlinks.referring_domains.live` | `$0.024072` | `$0.024036` | requested limit 2, returned 1 row | 2,921 bytes, SHA-256 `d1e83a6f24b9bfdb4f36b6398ecdbe960a58c45337e47c5f0dc4f451d19d223b` | +| `dataforseo.serp.google.organic.live.advanced` | `$0.002000` | `$0.002000` | one page, 11 SERP elements | 25,043 bytes, SHA-256 `f557cc9f963a888d47f8d4ac333fef873ed71f955290f90f9f06227aba26e6cb` | Actual Slice 2 live cost was `$0.050072`, below the `$0.050108` aggregate authorization bound. Every call succeeded on its first dispatch. diff --git a/packages/gkit/evals/slice4-google-ads-baseline.md b/packages/gkit/evals/slice4-google-ads-baseline.md index c7f1b43..caaf679 100644 --- a/packages/gkit/evals/slice4-google-ads-baseline.md +++ b/packages/gkit/evals/slice4-google-ads-baseline.md @@ -48,14 +48,14 @@ and argv contracts until the separate real-MCC gate passes. ## Live gate -| Capability | Pages | Rows | Artifact bytes | -| --- | ---: | ---: | ---: | -| `google-ads.customers.list-accessible` | 1 | 1 | 58 | -| `google-ads.fields.describe` | 1 | 1 | 280 | -| `google-ads.fields.search` | 2 | 2 | 662 | -| `google-ads.query.gaql` | 1 | 12 | 2,654 | -| `google-ads.keyword-plan.generate-ideas` | 1 | 12 | 20,917 | -| `google-ads.keyword-plan.generate-historical-metrics` | 1 | 1 | 1,904 | +| Capability | Pages | Rows | Artifact bytes | +| ----------------------------------------------------- | ----: | ---: | -------------: | +| `google-ads.customers.list-accessible` | 1 | 1 | 58 | +| `google-ads.fields.describe` | 1 | 1 | 280 | +| `google-ads.fields.search` | 2 | 2 | 662 | +| `google-ads.query.gaql` | 1 | 12 | 2,654 | +| `google-ads.keyword-plan.generate-ideas` | 1 | 12 | 20,917 | +| `google-ads.keyword-plan.generate-historical-metrics` | 1 | 1 | 1,904 | The field search deliberately used a one-row page size and proved a real two-page `pageToken` loop. Each command returned a safe provider request ID and diff --git a/packages/gkit/evals/slice4-gsc-baseline.md b/packages/gkit/evals/slice4-gsc-baseline.md index 1b26a0a..328a659 100644 --- a/packages/gkit/evals/slice4-gsc-baseline.md +++ b/packages/gkit/evals/slice4-gsc-baseline.md @@ -32,13 +32,13 @@ reads, an inaccessible-property negative, and same-input legacy comparisons. ## Real and legacy comparison gate -| Capability | gkit rows | Legacy result | Artifact bytes | -| --- | ---: | --- | ---: | -| `gsc.properties.list` | 2 | 2 properties | 218 | -| `gsc.search-analytics.query` | 25 | same 25-row request | 4,393 | -| `gsc.sitemaps.list` | 1 | same one-sitemap result | 424 | -| `gsc.sitemaps.get` | 1 | same selected sitemap object | 337 | -| `gsc.url-inspection.inspect` | 1 | same indexed URL result | 930 | +| Capability | gkit rows | Legacy result | Artifact bytes | +| ---------------------------- | --------: | ---------------------------- | -------------: | +| `gsc.properties.list` | 2 | 2 properties | 218 | +| `gsc.search-analytics.query` | 25 | same 25-row request | 4,393 | +| `gsc.sitemaps.list` | 1 | same one-sitemap result | 424 | +| `gsc.sitemaps.get` | 1 | same selected sitemap object | 337 | +| `gsc.url-inspection.inspect` | 1 | same indexed URL result | 930 | The negative Search Analytics request used an inaccessible property. It exited 1 with HTTP `403`, `AUTH_FAILED`, `outcome:"confirmed"`, and a 474-byte raw diff --git a/packages/gkit/evals/slice5-baseline.md b/packages/gkit/evals/slice5-baseline.md index 5c04927..4435c2a 100644 --- a/packages/gkit/evals/slice5-baseline.md +++ b/packages/gkit/evals/slice5-baseline.md @@ -17,13 +17,13 @@ resource: ../../../docs/plans/2026-07-13-gkit-vertical-slice-plan.md `bun run --filter gkit eval:slice5` passed the committed answer keys and observations: -| Metric | Result | Gate | -| --- | ---: | ---: | -| Task distribution | 10 explicit / 15 business / 10 long-tail / 5 negative | exact | -| Provider top-1 | 100% (35/35) | at least 95% | -| Discovery within two steps | 100% (35/35) | at least 90% | -| First executable command | 100% (33/33) | at least 90% | -| Negative precision | 100% (5/5) | at least 95% | +| Metric | Result | Gate | +| -------------------------- | ----------------------------------------------------: | -----------: | +| Task distribution | 10 explicit / 15 business / 10 long-tail / 5 negative | exact | +| Provider top-1 | 100% (35/35) | at least 95% | +| Discovery within two steps | 100% (35/35) | at least 90% | +| First executable command | 100% (33/33) | at least 90% | +| Negative precision | 100% (5/5) | at least 95% | Two positive tasks are inventory-only DataForSEO LLM Mention requests, so they correctly have no executable-command denominator. All 33 executable positive diff --git a/packages/gkit/evals/slice5-final-migration-matrix.md b/packages/gkit/evals/slice5-final-migration-matrix.md index 0c7f3d5..a80d3e7 100644 --- a/packages/gkit/evals/slice5-final-migration-matrix.md +++ b/packages/gkit/evals/slice5-final-migration-matrix.md @@ -29,25 +29,25 @@ replaced. Their final transformation is deterministic: ## Provider packages -| Deleted package | Replace | Final drop | Evidence | -| --- | ---: | ---: | --- | -| `@deniffer/backlink-cli` | 3 | 4 | [`dataforseo-migration-matrix.md`](./dataforseo-migration-matrix.md) | -| `@deniffer/serp-snapshot-cli` | 2 | 1 | [`dataforseo-migration-matrix.md`](./dataforseo-migration-matrix.md) | -| `@deniffer/ai-optimization-cli` | 1 | 9 | [`dataforseo-migration-matrix.md`](./dataforseo-migration-matrix.md) | -| `@deniffer/posthog-cli` | 1 | 12 | [`posthog-migration-matrix.md`](./posthog-migration-matrix.md) | -| `@deniffer/google-ads-cli` | 4 | 6 | [`google-ads-migration-matrix.md`](./google-ads-migration-matrix.md) | -| `@deniffer/gsc-cli` | 5 | 4 | [`gsc-migration-matrix.md`](./gsc-migration-matrix.md) | -| `@deniffer/bing-webmaster-cli` | 17 | 1 | [`bing-migration-matrix.md`](./bing-migration-matrix.md) | -| **Provider total** | **33** | **37** | all seven packages deleted | +| Deleted package | Replace | Final drop | Evidence | +| ------------------------------- | ------: | ---------: | -------------------------------------------------------------------- | +| `@deniffer/backlink-cli` | 3 | 4 | [`dataforseo-migration-matrix.md`](./dataforseo-migration-matrix.md) | +| `@deniffer/serp-snapshot-cli` | 2 | 1 | [`dataforseo-migration-matrix.md`](./dataforseo-migration-matrix.md) | +| `@deniffer/ai-optimization-cli` | 1 | 9 | [`dataforseo-migration-matrix.md`](./dataforseo-migration-matrix.md) | +| `@deniffer/posthog-cli` | 1 | 12 | [`posthog-migration-matrix.md`](./posthog-migration-matrix.md) | +| `@deniffer/google-ads-cli` | 4 | 6 | [`google-ads-migration-matrix.md`](./google-ads-migration-matrix.md) | +| `@deniffer/gsc-cli` | 5 | 4 | [`gsc-migration-matrix.md`](./gsc-migration-matrix.md) | +| `@deniffer/bing-webmaster-cli` | 17 | 1 | [`bing-migration-matrix.md`](./bing-migration-matrix.md) | +| **Provider total** | **33** | **37** | all seven packages deleted | ## Non-provider tools -| Deleted command | Final decision | Consequence accepted by sole consumer | -| --- | --- | --- | -| `page-extract page entity extract` | drop | normalized ctx-backed SEO/GEO artifact removed; use ctx directly when needed | -| `sitemap-watch registry dataset competitors` | drop | local competitor registry command removed | -| `sitemap-watch snapshot dataset pages` | drop | recursive normalized sitemap snapshot command removed | -| `sitemap-watch snapshot entity page` | drop | single-page snapshot selection removed | +| Deleted command | Final decision | Consequence accepted by sole consumer | +| -------------------------------------------- | -------------- | ---------------------------------------------------------------------------- | +| `page-extract page entity extract` | drop | normalized ctx-backed SEO/GEO artifact removed; use ctx directly when needed | +| `sitemap-watch registry dataset competitors` | drop | local competitor registry command removed | +| `sitemap-watch snapshot dataset pages` | drop | recursive normalized sitemap snapshot command removed | +| `sitemap-watch snapshot entity page` | drop | single-page snapshot selection removed | Final old-command total: **33 replace / 41 drop**. The repository now contains only `packages/gkit`; the private gkit package stays in the workspace layout. diff --git a/packages/gkit/package.json b/packages/gkit/package.json index 7c5c9d6..a76f4d9 100644 --- a/packages/gkit/package.json +++ b/packages/gkit/package.json @@ -1,57 +1,57 @@ { - "name": "gkit", - "version": "0.5.0", - "private": true, - "description": "Profile-bound, agent-first growth provider CLI.", - "bin": { - "gkit": "dist/gkit.js" - }, - "files": [ - "dist", - "docs", - "generated" - ], - "type": "module", - "sideEffects": false, - "scripts": { - "cli": "bun run ./src/cli.ts", - "build": "bun build ./bin/gkit.js --target=bun --minify --define GKIT_STANDALONE=true --outfile=dist/gkit.js && chmod +x dist/gkit.js", - "check-types": "tsc --noEmit", - "generate:dataforseo": "bun run ./scripts/generate-dataforseo.ts", - "generate:dataforseo:check": "bun run ./scripts/generate-dataforseo.ts --check", - "generate:bing": "bun run ./scripts/generate-bing.ts", - "generate:bing:check": "bun run ./scripts/generate-bing.ts --check", - "generate:google-ads": "bun run ./scripts/generate-google-ads.ts", - "generate:google-ads:check": "bun run ./scripts/generate-google-ads.ts --check", - "generate:gsc": "bun run ./scripts/generate-gsc.ts", - "generate:gsc:check": "bun run ./scripts/generate-gsc.ts --check", - "generate:hubspot": "bun run ./scripts/generate-hubspot.ts", - "generate:hubspot:check": "bun run ./scripts/generate-hubspot.ts --check", - "generate:pagespeed": "bun run ./scripts/generate-pagespeed.ts", - "generate:pagespeed:check": "bun run ./scripts/generate-pagespeed.ts --check", - "generate:posthog": "bun run ./scripts/generate-posthog.ts", - "generate:posthog:check": "bun run ./scripts/generate-posthog.ts --check", - "eval:slice5": "bun run ./src/eval.ts", - "test": "vitest", - "test:once": "vitest run && bun run test:site", - "test:site": "bun run ./scripts/test-site-snapshot.ts" - }, - "devDependencies": { - "@celados/argc": "^11.0.0", - "@standard-schema/spec": "^1.0.0", - "@types/bun": "^1.3.10", - "@types/node": "^25.6.0", - "@valibot/to-json-schema": "^1.6.0", - "ajv": "^8.20.0", - "bun-types": "^1.3.13", - "cheerio": "^1.2.0", - "dotenv": "^17.2.2", - "google-auth-library": "^10.9.0", - "robots-parser": "3.0.1", - "saxes": "6.0.0", - "typescript": "^5", - "valibot": "^1.2.0", - "vitest": "^4.0.18", - "yaml": "^2.9.0" - } + "name": "gkit", + "version": "0.5.0", + "private": true, + "description": "Profile-bound, agent-first growth provider CLI.", + "bin": { + "gkit": "dist/gkit.js" + }, + "files": [ + "dist", + "docs", + "generated" + ], + "type": "module", + "sideEffects": false, + "scripts": { + "cli": "bun run ./src/cli.ts", + "build": "bun build ./bin/gkit.js --target=bun --minify --define GKIT_STANDALONE=true --outfile=dist/gkit.js && chmod +x dist/gkit.js", + "generate:dataforseo": "bun run ./scripts/generate-dataforseo.ts", + "generate:dataforseo:check": "bun run ./scripts/generate-dataforseo.ts --check", + "generate:bing": "bun run ./scripts/generate-bing.ts", + "generate:bing:check": "bun run ./scripts/generate-bing.ts --check", + "generate:google-ads": "bun run ./scripts/generate-google-ads.ts", + "generate:google-ads:check": "bun run ./scripts/generate-google-ads.ts --check", + "generate:gsc": "bun run ./scripts/generate-gsc.ts", + "generate:gsc:check": "bun run ./scripts/generate-gsc.ts --check", + "generate:hubspot": "bun run ./scripts/generate-hubspot.ts", + "generate:hubspot:check": "bun run ./scripts/generate-hubspot.ts --check", + "generate:pagespeed": "bun run ./scripts/generate-pagespeed.ts", + "generate:pagespeed:check": "bun run ./scripts/generate-pagespeed.ts --check", + "generate:posthog": "bun run ./scripts/generate-posthog.ts", + "generate:posthog:check": "bun run ./scripts/generate-posthog.ts --check", + "eval:slice5": "bun run ./src/eval.ts", + "test": "vp test", + "test:once": "vp test run && bun run test:site", + "test:site": "bun run ./scripts/test-site-snapshot.ts", + "generate:check": "bun run generate:dataforseo:check && bun run generate:bing:check && bun run generate:google-ads:check && bun run generate:gsc:check && bun run generate:hubspot:check && bun run generate:pagespeed:check && bun run generate:posthog:check" + }, + "devDependencies": { + "@celados/argc": "^11.0.0", + "@standard-schema/spec": "^1.0.0", + "@types/bun": "^1.3.10", + "@types/node": "^25.6.0", + "@valibot/to-json-schema": "^1.6.0", + "ajv": "^8.20.0", + "bun-types": "^1.3.13", + "cheerio": "^1.2.0", + "dotenv": "^17.2.2", + "google-auth-library": "^10.9.0", + "robots-parser": "3.0.1", + "saxes": "6.0.0", + "typescript": "^7.0.2", + "valibot": "^1.2.0", + "vitest": "^4.0.18", + "yaml": "^2.9.0" + } } diff --git a/packages/gkit/policy/bing.reviewed.json b/packages/gkit/policy/bing.reviewed.json index ef0c8b6..8e8a775 100644 --- a/packages/gkit/policy/bing.reviewed.json +++ b/packages/gkit/policy/bing.reviewed.json @@ -1,658 +1,675 @@ { - "version": 1, - "provider": "bing", - "manifestRevision": "2026-10-27.bing.search-health.1", - "reviewedAt": "2026-10-27T04:00:00.000Z", - "entries": [ - { - "path": "/GetUserSites", - "method": "get", - "operationId": "GetUserSites", - "exposure": "executable", - "capability": { - "id": "bing.sites.list", - "provider": "bing", - "revision": "1", - "adapterKey": "sites.list", - "title": "List accessible Bing Webmaster sites", - "description": "List every site accessible to the profile-bound Bing Webmaster API key.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": {} - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.sites.list --input '{}' --out bing-sites.json --dry-run" - } - ] - } - }, - { - "path": "/GetRankAndTrafficStats", - "method": "get", - "operationId": "GetRankAndTrafficStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.rank", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.rank", - "title": "Read Bing rank and traffic statistics", - "description": "Read rank and traffic statistics for the profile-bound or explicitly selected site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.traffic.rank --input '{}' --out bing-rank.json --dry-run" - } - ] - } - }, - { - "path": "/GetQueryStats", - "method": "get", - "operationId": "GetQueryStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.queries", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.queries", - "title": "Read top Bing query statistics", - "description": "Read top query traffic statistics for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.traffic.queries --input '{}' --out bing-queries.json --dry-run" - } - ] - } - }, - { - "path": "/GetPageStats", - "method": "get", - "operationId": "GetPageStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.pages", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.pages", - "title": "Read top Bing page statistics", - "description": "Read top page traffic statistics for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.traffic.pages --input '{}' --out bing-pages.json --dry-run" - } - ] - } - }, - { - "path": "/GetQueryTrafficStats", - "method": "get", - "operationId": "GetQueryTrafficStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.query", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.query", - "title": "Read one Bing query's traffic", - "description": "Read daily traffic statistics for one exact query.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["query"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "query": { "type": "string", "minLength": 1, "maxLength": 512 } - } - }, - "examples": [ - { - "input": { "query": "website cloner" }, - "command": "gkit --profile bing api call --capability-id bing.traffic.query --input @request.json --out bing-query.json --dry-run" - } - ] - } - }, - { - "path": "/GetPageQueryStats", - "method": "get", - "operationId": "GetPageQueryStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.page-queries", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.page-queries", - "title": "Read Bing queries for one page", - "description": "Read query statistics for one exact page URL.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["pageUrl"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "pageUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "input": { "pageUrl": "https://example.com/docs" }, - "command": "gkit --profile bing api call --capability-id bing.traffic.page-queries --input @request.json --out bing-page-queries.json --dry-run" - } - ] - } - }, - { - "path": "/GetQueryPageStats", - "method": "get", - "operationId": "GetQueryPageStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.query-pages", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.query-pages", - "title": "Read Bing pages for one query", - "description": "Read page statistics for one exact query.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["query"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "query": { "type": "string", "minLength": 1, "maxLength": 512 } - } - }, - "examples": [ - { - "input": { "query": "website cloner" }, - "command": "gkit --profile bing api call --capability-id bing.traffic.query-pages --input @request.json --out bing-query-pages.json --dry-run" - } - ] - } - }, - { - "path": "/GetQueryPageDetailStats", - "method": "get", - "operationId": "GetQueryPageDetailStats", - "exposure": "executable", - "capability": { - "id": "bing.traffic.query-page", - "provider": "bing", - "revision": "1", - "adapterKey": "traffic.query-page", - "title": "Read detailed Bing query-page statistics", - "description": "Read detailed statistics for one exact query and page URL pair.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["query", "pageUrl"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "query": { "type": "string", "minLength": 1, "maxLength": 512 }, - "pageUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "input": { "query": "website cloner", "pageUrl": "https://example.com/docs" }, - "command": "gkit --profile bing api call --capability-id bing.traffic.query-page --input @request.json --out bing-query-page.json --dry-run" - } - ] - } - }, - { - "path": "/GetCrawlStats", - "method": "get", - "operationId": "GetCrawlStats", - "exposure": "executable", - "capability": { - "id": "bing.crawl.stats", - "provider": "bing", - "revision": "1", - "adapterKey": "crawl.stats", - "title": "Read Bing crawl statistics", - "description": "Read crawl statistics for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.crawl.stats --input '{}' --out bing-crawl-stats.json --dry-run" - } - ] - } - }, - { - "path": "/GetCrawlIssues", - "method": "get", - "operationId": "GetCrawlIssues", - "exposure": "executable", - "capability": { - "id": "bing.crawl.issues", - "provider": "bing", - "revision": "1", - "adapterKey": "crawl.issues", - "title": "Read Bing crawl issues", - "description": "Read current crawl issue URLs for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.crawl.issues --input '{}' --out bing-crawl-issues.json --dry-run" - } - ] - } - }, - { - "path": "/GetCrawlSettings", - "method": "get", - "operationId": "GetCrawlSettings", - "exposure": "executable", - "capability": { - "id": "bing.crawl.settings", - "provider": "bing", - "revision": "1", - "adapterKey": "crawl.settings", - "title": "Read Bing crawl settings", - "description": "Read crawl settings for one site without exposing mutation methods.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.crawl.settings --input '{}' --out bing-crawl-settings.json --dry-run" - } - ] - } - }, - { - "path": "/GetLinkCounts", - "method": "get", - "operationId": "GetLinkCounts", - "exposure": "executable", - "capability": { - "id": "bing.links.pages", - "provider": "bing", - "revision": "1", - "adapterKey": "links.pages", - "title": "Read Bing inbound link counts", - "description": "Read one provider page of URLs and inbound link counts.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "page": { "type": "integer", "minimum": 0, "maximum": 100000 } - } - }, - "examples": [ - { - "input": { "page": 0 }, - "command": "gkit --profile bing api call --capability-id bing.links.pages --input '{\"page\":0}' --out bing-links.json --dry-run" - } - ] - } - }, - { - "path": "/GetUrlLinks", - "method": "get", - "operationId": "GetUrlLinks", - "exposure": "executable", - "capability": { - "id": "bing.links.url", - "provider": "bing", - "revision": "1", - "adapterKey": "links.url", - "title": "Read Bing inbound links for one URL", - "description": "Read one provider page of inbound link details for an exact URL.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["link"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "link": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "page": { "type": "integer", "minimum": 0, "maximum": 100000 } - } - }, - "examples": [ - { - "input": { "link": "https://example.com/docs", "page": 0 }, - "command": "gkit --profile bing api call --capability-id bing.links.url --input @request.json --out bing-url-links.json --dry-run" - } - ] - } - }, - { - "path": "/GetFeeds", - "method": "get", - "operationId": "GetFeeds", - "exposure": "executable", - "capability": { - "id": "bing.sitemaps.list", - "provider": "bing", - "revision": "1", - "adapterKey": "sitemaps.list", - "title": "List Bing feeds and sitemaps", - "description": "List top-level feeds or sitemaps for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.sitemaps.list --input '{}' --out bing-sitemaps.json --dry-run" - } - ] - } - }, - { - "path": "/GetFeedDetails", - "method": "get", - "operationId": "GetFeedDetails", - "exposure": "executable", - "capability": { - "id": "bing.sitemaps.get", - "provider": "bing", - "revision": "1", - "adapterKey": "sitemaps.get", - "title": "Read one Bing feed", - "description": "Read details for one exact sitemap or feed URL.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["feedUrl"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "feedUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "input": { "feedUrl": "https://example.com/sitemap.xml" }, - "command": "gkit --profile bing api call --capability-id bing.sitemaps.get --input @request.json --out bing-sitemap.json --dry-run" - } - ] - } - }, - { - "path": "/GetUrlInfo", - "method": "get", - "operationId": "GetUrlInfo", - "exposure": "executable", - "capability": { - "id": "bing.urls.info", - "provider": "bing", - "revision": "1", - "adapterKey": "urls.info", - "title": "Read Bing index information for a URL", - "description": "Read Bing index information for one URL or domain target.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["url"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "url": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "input": { "url": "https://example.com/docs" }, - "command": "gkit --profile bing api call --capability-id bing.urls.info --input @request.json --out bing-url-info.json --dry-run" - } - ] - } - }, - { - "path": "/GetUrlTrafficInfo", - "method": "get", - "operationId": "GetUrlTrafficInfo", - "exposure": "executable", - "capability": { - "id": "bing.urls.traffic", - "provider": "bing", - "revision": "1", - "adapterKey": "urls.traffic", - "title": "Read Bing traffic for a URL", - "description": "Read Bing traffic information for one URL or domain target.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["url"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "url": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "input": { "url": "https://example.com/docs" }, - "command": "gkit --profile bing api call --capability-id bing.urls.traffic --input @request.json --out bing-url-traffic.json --dry-run" - } - ] - } - }, - { - "path": "/GetBlockedUrls", - "method": "get", - "operationId": "GetBlockedUrls", - "exposure": "executable", - "capability": { - "id": "bing.crawl.blocked-urls", - "provider": "bing", - "revision": "1", - "adapterKey": "crawl.blocked-urls", - "title": "Read Bing blocked URLs", - "description": "Read URLs and directories blocked from Bing results for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { - "siteUrl": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - } - } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.crawl.blocked-urls --input '{}' --out bing-blocked-urls.json --dry-run" - } - ] - } - }, - { - "path": "/GetUrlSubmissionQuota", - "method": "get", - "operationId": "GetUrlSubmissionQuota", - "exposure": "executable", - "capability": { - "id": "bing.urls.submission-quota", - "provider": "bing", - "revision": "1", - "adapterKey": "urls.submission-quota", - "title": "Read Bing URL submission quota", - "description": "Read the remaining daily and monthly URL submission quota for one site.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { - "siteUrl": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - } - } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile bing api call --capability-id bing.urls.submission-quota --input '{}' --out bing-url-quota.json --dry-run" - } - ] - } - }, - { - "path": "/SubmitUrlbatch", - "method": "post", - "operationId": "SubmitUrlbatch", - "exposure": "executable", - "capability": { - "id": "bing.urls.submit", - "provider": "bing", - "revision": "1", - "adapterKey": "urls.submit", - "title": "Submit URLs to Bing", - "description": "Submit up to 500 URLs of one site for Bing crawling; consumes the daily URL submission quota.", - "effects": ["write"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["urlList"], - "properties": { - "siteUrl": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - }, - "urlList": { - "type": "array", - "minItems": 1, - "maxItems": 500, - "uniqueItems": true, - "items": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - } - } - } - }, - "examples": [ - { - "input": { - "urlList": ["https://example.com/new-page"] - }, - "command": "gkit --profile bing api call --capability-id bing.urls.submit --input @request.json --allow-write --out bing-url-submit.json --dry-run" - } - ] - } - }, - { - "path": "/SubmitFeed", - "method": "post", - "operationId": "SubmitFeed", - "exposure": "executable", - "capability": { - "id": "bing.sitemaps.submit", - "provider": "bing", - "revision": "1", - "adapterKey": "sitemaps.submit", - "title": "Submit a sitemap to Bing", - "description": "Submit or resubmit one sitemap or feed URL for one site.", - "effects": ["write"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["feedUrl"], - "properties": { - "siteUrl": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - }, - "feedUrl": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - } - } - }, - "examples": [ - { - "input": { - "feedUrl": "https://example.com/sitemap.xml" - }, - "command": "gkit --profile bing api call --capability-id bing.sitemaps.submit --input @request.json --allow-write --out bing-sitemap-submit.json --dry-run" - } - ] - } - } - ] + "version": 1, + "provider": "bing", + "manifestRevision": "2026-10-27.bing.search-health.1", + "reviewedAt": "2026-10-27T04:00:00.000Z", + "entries": [ + { + "path": "/GetUserSites", + "method": "get", + "operationId": "GetUserSites", + "exposure": "executable", + "capability": { + "id": "bing.sites.list", + "provider": "bing", + "revision": "1", + "adapterKey": "sites.list", + "title": "List accessible Bing Webmaster sites", + "description": "List every site accessible to the profile-bound Bing Webmaster API key.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.sites.list --input '{}' --out bing-sites.json --dry-run" + } + ] + } + }, + { + "path": "/GetRankAndTrafficStats", + "method": "get", + "operationId": "GetRankAndTrafficStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.rank", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.rank", + "title": "Read Bing rank and traffic statistics", + "description": "Read rank and traffic statistics for the profile-bound or explicitly selected site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.traffic.rank --input '{}' --out bing-rank.json --dry-run" + } + ] + } + }, + { + "path": "/GetQueryStats", + "method": "get", + "operationId": "GetQueryStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.queries", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.queries", + "title": "Read top Bing query statistics", + "description": "Read top query traffic statistics for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.traffic.queries --input '{}' --out bing-queries.json --dry-run" + } + ] + } + }, + { + "path": "/GetPageStats", + "method": "get", + "operationId": "GetPageStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.pages", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.pages", + "title": "Read top Bing page statistics", + "description": "Read top page traffic statistics for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.traffic.pages --input '{}' --out bing-pages.json --dry-run" + } + ] + } + }, + { + "path": "/GetQueryTrafficStats", + "method": "get", + "operationId": "GetQueryTrafficStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.query", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.query", + "title": "Read one Bing query's traffic", + "description": "Read daily traffic statistics for one exact query.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["query"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "query": { "type": "string", "minLength": 1, "maxLength": 512 } + } + }, + "examples": [ + { + "input": { "query": "website cloner" }, + "command": "gkit --profile bing api call --capability-id bing.traffic.query --input @request.json --out bing-query.json --dry-run" + } + ] + } + }, + { + "path": "/GetPageQueryStats", + "method": "get", + "operationId": "GetPageQueryStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.page-queries", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.page-queries", + "title": "Read Bing queries for one page", + "description": "Read query statistics for one exact page URL.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["pageUrl"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "pageUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": { "pageUrl": "https://example.com/docs" }, + "command": "gkit --profile bing api call --capability-id bing.traffic.page-queries --input @request.json --out bing-page-queries.json --dry-run" + } + ] + } + }, + { + "path": "/GetQueryPageStats", + "method": "get", + "operationId": "GetQueryPageStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.query-pages", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.query-pages", + "title": "Read Bing pages for one query", + "description": "Read page statistics for one exact query.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["query"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "query": { "type": "string", "minLength": 1, "maxLength": 512 } + } + }, + "examples": [ + { + "input": { "query": "website cloner" }, + "command": "gkit --profile bing api call --capability-id bing.traffic.query-pages --input @request.json --out bing-query-pages.json --dry-run" + } + ] + } + }, + { + "path": "/GetQueryPageDetailStats", + "method": "get", + "operationId": "GetQueryPageDetailStats", + "exposure": "executable", + "capability": { + "id": "bing.traffic.query-page", + "provider": "bing", + "revision": "1", + "adapterKey": "traffic.query-page", + "title": "Read detailed Bing query-page statistics", + "description": "Read detailed statistics for one exact query and page URL pair.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["query", "pageUrl"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "query": { "type": "string", "minLength": 1, "maxLength": 512 }, + "pageUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": { + "query": "website cloner", + "pageUrl": "https://example.com/docs" + }, + "command": "gkit --profile bing api call --capability-id bing.traffic.query-page --input @request.json --out bing-query-page.json --dry-run" + } + ] + } + }, + { + "path": "/GetCrawlStats", + "method": "get", + "operationId": "GetCrawlStats", + "exposure": "executable", + "capability": { + "id": "bing.crawl.stats", + "provider": "bing", + "revision": "1", + "adapterKey": "crawl.stats", + "title": "Read Bing crawl statistics", + "description": "Read crawl statistics for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.crawl.stats --input '{}' --out bing-crawl-stats.json --dry-run" + } + ] + } + }, + { + "path": "/GetCrawlIssues", + "method": "get", + "operationId": "GetCrawlIssues", + "exposure": "executable", + "capability": { + "id": "bing.crawl.issues", + "provider": "bing", + "revision": "1", + "adapterKey": "crawl.issues", + "title": "Read Bing crawl issues", + "description": "Read current crawl issue URLs for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.crawl.issues --input '{}' --out bing-crawl-issues.json --dry-run" + } + ] + } + }, + { + "path": "/GetCrawlSettings", + "method": "get", + "operationId": "GetCrawlSettings", + "exposure": "executable", + "capability": { + "id": "bing.crawl.settings", + "provider": "bing", + "revision": "1", + "adapterKey": "crawl.settings", + "title": "Read Bing crawl settings", + "description": "Read crawl settings for one site without exposing mutation methods.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.crawl.settings --input '{}' --out bing-crawl-settings.json --dry-run" + } + ] + } + }, + { + "path": "/GetLinkCounts", + "method": "get", + "operationId": "GetLinkCounts", + "exposure": "executable", + "capability": { + "id": "bing.links.pages", + "provider": "bing", + "revision": "1", + "adapterKey": "links.pages", + "title": "Read Bing inbound link counts", + "description": "Read one provider page of URLs and inbound link counts.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "page": { "type": "integer", "minimum": 0, "maximum": 100000 } + } + }, + "examples": [ + { + "input": { "page": 0 }, + "command": "gkit --profile bing api call --capability-id bing.links.pages --input '{\"page\":0}' --out bing-links.json --dry-run" + } + ] + } + }, + { + "path": "/GetUrlLinks", + "method": "get", + "operationId": "GetUrlLinks", + "exposure": "executable", + "capability": { + "id": "bing.links.url", + "provider": "bing", + "revision": "1", + "adapterKey": "links.url", + "title": "Read Bing inbound links for one URL", + "description": "Read one provider page of inbound link details for an exact URL.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["link"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "link": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "page": { "type": "integer", "minimum": 0, "maximum": 100000 } + } + }, + "examples": [ + { + "input": { "link": "https://example.com/docs", "page": 0 }, + "command": "gkit --profile bing api call --capability-id bing.links.url --input @request.json --out bing-url-links.json --dry-run" + } + ] + } + }, + { + "path": "/GetFeeds", + "method": "get", + "operationId": "GetFeeds", + "exposure": "executable", + "capability": { + "id": "bing.sitemaps.list", + "provider": "bing", + "revision": "1", + "adapterKey": "sitemaps.list", + "title": "List Bing feeds and sitemaps", + "description": "List top-level feeds or sitemaps for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.sitemaps.list --input '{}' --out bing-sitemaps.json --dry-run" + } + ] + } + }, + { + "path": "/GetFeedDetails", + "method": "get", + "operationId": "GetFeedDetails", + "exposure": "executable", + "capability": { + "id": "bing.sitemaps.get", + "provider": "bing", + "revision": "1", + "adapterKey": "sitemaps.get", + "title": "Read one Bing feed", + "description": "Read details for one exact sitemap or feed URL.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["feedUrl"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "feedUrl": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": { "feedUrl": "https://example.com/sitemap.xml" }, + "command": "gkit --profile bing api call --capability-id bing.sitemaps.get --input @request.json --out bing-sitemap.json --dry-run" + } + ] + } + }, + { + "path": "/GetUrlInfo", + "method": "get", + "operationId": "GetUrlInfo", + "exposure": "executable", + "capability": { + "id": "bing.urls.info", + "provider": "bing", + "revision": "1", + "adapterKey": "urls.info", + "title": "Read Bing index information for a URL", + "description": "Read Bing index information for one URL or domain target.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["url"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "url": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": { "url": "https://example.com/docs" }, + "command": "gkit --profile bing api call --capability-id bing.urls.info --input @request.json --out bing-url-info.json --dry-run" + } + ] + } + }, + { + "path": "/GetUrlTrafficInfo", + "method": "get", + "operationId": "GetUrlTrafficInfo", + "exposure": "executable", + "capability": { + "id": "bing.urls.traffic", + "provider": "bing", + "revision": "1", + "adapterKey": "urls.traffic", + "title": "Read Bing traffic for a URL", + "description": "Read Bing traffic information for one URL or domain target.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["url"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "url": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "input": { "url": "https://example.com/docs" }, + "command": "gkit --profile bing api call --capability-id bing.urls.traffic --input @request.json --out bing-url-traffic.json --dry-run" + } + ] + } + }, + { + "path": "/GetBlockedUrls", + "method": "get", + "operationId": "GetBlockedUrls", + "exposure": "executable", + "capability": { + "id": "bing.crawl.blocked-urls", + "provider": "bing", + "revision": "1", + "adapterKey": "crawl.blocked-urls", + "title": "Read Bing blocked URLs", + "description": "Read URLs and directories blocked from Bing results for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.crawl.blocked-urls --input '{}' --out bing-blocked-urls.json --dry-run" + } + ] + } + }, + { + "path": "/GetUrlSubmissionQuota", + "method": "get", + "operationId": "GetUrlSubmissionQuota", + "exposure": "executable", + "capability": { + "id": "bing.urls.submission-quota", + "provider": "bing", + "revision": "1", + "adapterKey": "urls.submission-quota", + "title": "Read Bing URL submission quota", + "description": "Read the remaining daily and monthly URL submission quota for one site.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile bing api call --capability-id bing.urls.submission-quota --input '{}' --out bing-url-quota.json --dry-run" + } + ] + } + }, + { + "path": "/SubmitUrlbatch", + "method": "post", + "operationId": "SubmitUrlbatch", + "exposure": "executable", + "capability": { + "id": "bing.urls.submit", + "provider": "bing", + "revision": "1", + "adapterKey": "urls.submit", + "title": "Submit URLs to Bing", + "description": "Submit up to 500 URLs of one site for Bing crawling; consumes the daily URL submission quota.", + "effects": ["write"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["urlList"], + "properties": { + "siteUrl": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "urlList": { + "type": "array", + "minItems": 1, + "maxItems": 500, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + } + }, + "examples": [ + { + "input": { + "urlList": ["https://example.com/new-page"] + }, + "command": "gkit --profile bing api call --capability-id bing.urls.submit --input @request.json --allow-write --out bing-url-submit.json --dry-run" + } + ] + } + }, + { + "path": "/SubmitFeed", + "method": "post", + "operationId": "SubmitFeed", + "exposure": "executable", + "capability": { + "id": "bing.sitemaps.submit", + "provider": "bing", + "revision": "1", + "adapterKey": "sitemaps.submit", + "title": "Submit a sitemap to Bing", + "description": "Submit or resubmit one sitemap or feed URL for one site.", + "effects": ["write"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["feedUrl"], + "properties": { + "siteUrl": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "feedUrl": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "examples": [ + { + "input": { + "feedUrl": "https://example.com/sitemap.xml" + }, + "command": "gkit --profile bing api call --capability-id bing.sitemaps.submit --input @request.json --allow-write --out bing-sitemap-submit.json --dry-run" + } + ] + } + } + ] } diff --git a/packages/gkit/policy/dataforseo.reviewed.json b/packages/gkit/policy/dataforseo.reviewed.json index 5f775b7..b59bacc 100644 --- a/packages/gkit/policy/dataforseo.reviewed.json +++ b/packages/gkit/policy/dataforseo.reviewed.json @@ -1,371 +1,440 @@ { - "version": 1, - "provider": "dataforseo", - "manifestRevision": "2026-07-15.llm-mentions.2", - "reviewedAt": "2026-07-15T04:00:00.000Z", - "entries": [ - { - "path": "/v3/backlinks/bulk_ranks/live", - "method": "post", - "operationId": "BulkRanksLive", - "exposure": "executable", - "capability": { - "id": "dataforseo.backlinks.bulk_ranks.live", - "provider": "dataforseo", - "revision": "2", - "title": "Bulk backlink ranks", - "description": "Return DataForSEO backlink ranks for up to 1,000 targets in one live request.", - "adapterKey": "backlinks.bulk_ranks.live", - "effects": ["read", "spend"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["targets"], - "properties": { - "targets": { - "type": "array", - "description": "Domains, subdomains, or absolute URLs to rank.", - "minItems": 1, - "maxItems": 1000, - "items": { "type": "string", "minLength": 1 } - }, - "rank_scale": { - "type": "string", - "enum": ["one_hundred", "one_thousand"] - }, - "tag": { "type": "string", "maxLength": 255 } - } - }, - "cost": { - "currency": "USD", - "policyRevision": "dataforseo-backlinks-pricing-2026-07-14-v1", - "model": { - "type": "linear-items", - "baseMicros": 24000, - "perItemMicros": 36, - "itemsJsonPointer": "/targets", - "maxItems": 1000 - } - }, - "examples": [ - { - "title": "Safe preflight", - "input": { - "targets": ["clonesite.ai", "example.com"], - "rank_scale": "one_hundred", - "tag": "slice2-smoke" - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.bulk_ranks.live --input @req.json --allow-spend --max-spend-usd 0.024072 --out ranks.json --dry-run" - }, - { - "title": "Live call after reviewing the dry-run", - "input": { - "targets": ["clonesite.ai", "example.com"], - "rank_scale": "one_hundred", - "tag": "slice2-smoke" - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.bulk_ranks.live --input @req.json --allow-spend --max-spend-usd 0.024072 --out ranks.json" - } - ] - } - }, - { - "path": "/v3/backlinks/summary/live", - "method": "post", - "operationId": "SummaryLive", - "exposure": "executable", - "capability": { - "id": "dataforseo.backlinks.summary.live", - "provider": "dataforseo", - "revision": "1", - "title": "Backlink summary", - "description": "Return the reviewed live backlink summary for one domain, subdomain, or page.", - "adapterKey": "backlinks.summary.live", - "effects": ["read", "spend"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["target"], - "properties": { - "target": { "type": "string", "minLength": 1 }, - "include_subdomains": { "type": "boolean" }, - "exclude_internal_backlinks": { "type": "boolean" }, - "internal_list_limit": { "type": "integer", "minimum": 1, "maximum": 10 }, - "backlinks_status_type": { "type": "string", "enum": ["all", "live", "lost"] }, - "rank_scale": { "type": "string", "enum": ["one_hundred", "one_thousand"] }, - "tag": { "type": "string", "maxLength": 255 } - } - }, - "cost": { - "currency": "USD", - "policyRevision": "dataforseo-backlinks-pricing-2026-07-14-v1", - "model": { "type": "fixed", "micros": 24036 } - }, - "examples": [ - { - "title": "Safe preflight", - "input": { - "target": "clonesite.ai", - "include_subdomains": true, - "backlinks_status_type": "live" - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.summary.live --input @req.json --allow-spend --max-spend-usd 0.024036 --out summary.json --dry-run" - }, - { - "title": "Live call after reviewing the dry-run", - "input": { - "target": "clonesite.ai", - "include_subdomains": true, - "backlinks_status_type": "live" - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.summary.live --input @req.json --allow-spend --max-spend-usd 0.024036 --out summary.json" - } - ] - } - }, - { - "path": "/v3/backlinks/referring_domains/live", - "method": "post", - "operationId": "ReferringDomainsLive", - "exposure": "executable", - "capability": { - "id": "dataforseo.backlinks.referring_domains.live", - "provider": "dataforseo", - "revision": "1", - "title": "Referring domains", - "description": "Return up to 100 reviewed referring-domain rows for one target.", - "adapterKey": "backlinks.referring_domains.live", - "effects": ["read", "spend"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["target", "limit"], - "properties": { - "target": { "type": "string", "minLength": 1 }, - "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, - "order_by": { - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "string", - "enum": ["rank,asc", "rank,desc", "backlinks,asc", "backlinks,desc"] - } - }, - "include_subdomains": { "type": "boolean" }, - "exclude_internal_backlinks": { "type": "boolean" }, - "backlinks_status_type": { "type": "string", "enum": ["all", "live", "lost"] }, - "rank_scale": { "type": "string", "enum": ["one_hundred", "one_thousand"] }, - "tag": { "type": "string", "maxLength": 255 } - } - }, - "cost": { - "currency": "USD", - "policyRevision": "dataforseo-backlinks-pricing-2026-07-14-v1", - "model": { - "type": "linear-number", - "baseMicros": 24000, - "perUnitMicros": 36, - "valueJsonPointer": "/limit", - "maxValue": 100 - } - }, - "examples": [ - { - "title": "Safe preflight", - "input": { - "target": "clonesite.ai", - "limit": 20, - "order_by": ["rank,desc"], - "exclude_internal_backlinks": true - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.referring_domains.live --input @req.json --allow-spend --max-spend-usd 0.024720 --out referring-domains.json --dry-run" - }, - { - "title": "Live call after reviewing the dry-run", - "input": { - "target": "clonesite.ai", - "limit": 20, - "order_by": ["rank,desc"], - "exclude_internal_backlinks": true - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.referring_domains.live --input @req.json --allow-spend --max-spend-usd 0.024720 --out referring-domains.json" - } - ] - } - }, - { - "path": "/v3/serp/google/organic/live/advanced", - "method": "post", - "operationId": "GoogleOrganicLiveAdvanced", - "exposure": "executable", - "capability": { - "id": "dataforseo.serp.google.organic.live.advanced", - "provider": "dataforseo", - "revision": "1", - "title": "Google Organic live advanced SERP", - "description": "Return one Google Organic live advanced SERP with a reviewed maximum depth of 10.", - "adapterKey": "serp.google.organic.live.advanced", - "effects": ["read", "spend"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["keyword", "location_code", "language_code", "device", "os", "depth"], - "properties": { - "keyword": { - "type": "string", - "minLength": 1, - "maxLength": 700, - "not": { - "pattern": "(?:[Aa][Ll][Ll][Ii][Nn][Aa][Nn][Cc][Hh][Oo][Rr]|[Aa][Ll][Ll][Ii][Nn][Tt][Ee][Xx][Tt]|[Aa][Ll][Ll][Ii][Nn][Tt][Ii][Tt][Ll][Ee]|[Aa][Ll][Ll][Ii][Nn][Uu][Rr][Ll]|[Dd][Ee][Ff][Ii][Nn][Ee]|[Dd][Ee][Ff][Ii][Nn][Ii][Tt][Ii][Oo][Nn]|[Ff][Ii][Ll][Ee][Tt][Yy][Pp][Ee]|[Ii][Dd]|[Ii][Nn][Aa][Nn][Cc][Hh][Oo][Rr]|[Ii][Nn][Ff][Oo]|[Ii][Nn][Tt][Ee][Xx][Tt]|[Ii][Nn][Tt][Ii][Tt][Ll][Ee]|[Ii][Nn][Uu][Rr][Ll]|[Ll][Ii][Nn][Kk]|[Rr][Ee][Ll][Aa][Tt][Ee][Dd]|[Ss][Ii][Tt][Ee]|[Cc][Aa][Cc][Hh][Ee]):" - } - }, - "location_code": { "type": "integer", "minimum": 1 }, - "language_code": { "type": "string", "minLength": 2, "maxLength": 10 }, - "device": { "type": "string", "enum": ["desktop", "mobile"] }, - "os": { "type": "string", "enum": ["windows", "macos", "android", "ios"] }, - "depth": { "type": "integer", "minimum": 1, "maximum": 10 }, - "tag": { "type": "string", "maxLength": 255 } - }, - "allOf": [ - { - "if": { "properties": { "device": { "const": "desktop" } } }, - "then": { "properties": { "os": { "enum": ["windows", "macos"] } } }, - "else": { "properties": { "os": { "enum": ["android", "ios"] } } } - } - ] - }, - "cost": { - "currency": "USD", - "policyRevision": "dataforseo-google-organic-live-pricing-2026-07-14-v1", - "model": { "type": "fixed", "micros": 2000 } - }, - "examples": [ - { - "title": "Safe preflight", - "input": { - "keyword": "website cloner", - "location_code": 2840, - "language_code": "en", - "device": "desktop", - "os": "windows", - "depth": 10 - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.serp.google.organic.live.advanced --input @req.json --allow-spend --max-spend-usd 0.002000 --out serp.json --dry-run" - }, - { - "title": "Live call after reviewing the dry-run", - "input": { - "keyword": "website cloner", - "location_code": 2840, - "language_code": "en", - "device": "desktop", - "os": "windows", - "depth": 10 - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.serp.google.organic.live.advanced --input @req.json --allow-spend --max-spend-usd 0.002000 --out serp.json" - } - ] - } - }, - { - "path": "/v3/ai_optimization/llm_mentions/search/live", - "method": "post", - "operationId": "LlmMentionsSearchLive", - "exposure": "executable", - "capability": { - "id": "dataforseo.ai_optimization.llm_mentions.search.live", - "provider": "dataforseo", - "revision": "1", - "title": "LLM mentions search", - "description": "Search reviewed ChatGPT or Google AI mention records for one domain in United States English results.", - "adapterKey": "ai_optimization.llm_mentions.search.live", - "effects": ["read", "spend"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["target", "location_code", "language_code", "platform", "limit"], - "properties": { - "target": { - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "additionalProperties": false, - "required": ["domain"], - "properties": { - "domain": { - "type": "string", - "minLength": 1, - "maxLength": 63, - "pattern": "^(?!https?://)(?!www\\.)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,63}$" - }, - "search_filter": { "type": "string", "enum": ["include"] }, - "search_scope": { - "type": "array", - "minItems": 1, - "maxItems": 5, - "uniqueItems": true, - "items": { - "type": "string", - "enum": ["any", "question", "answer", "brand_entities", "fan_out_queries"] - } - }, - "include_subdomains": { "type": "boolean" } - } - } - }, - "location_code": { "type": "integer", "const": 2840 }, - "language_code": { "type": "string", "const": "en" }, - "platform": { "type": "string", "enum": ["chat_gpt", "google"] }, - "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, - "order_by": { - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { "type": "string", "enum": ["ai_search_volume,asc", "ai_search_volume,desc", "last_response_at,asc", "last_response_at,desc"] } - }, - "tag": { "type": "string", "maxLength": 255 } - } - }, - "cost": { - "currency": "USD", - "policyRevision": "dataforseo-ai-optimization-llm-mentions-pricing-2026-07-15-v2", - "model": { "type": "fixed", "micros": 150000 } - }, - "examples": [ - { - "title": "Safe preflight", - "input": { - "target": [{ "domain": "clonesite.ai", "search_filter": "include", "search_scope": ["any"] }], - "location_code": 2840, - "language_code": "en", - "platform": "chat_gpt", - "limit": 20, - "order_by": ["ai_search_volume,desc"] - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.ai_optimization.llm_mentions.search.live --input @req.json --allow-spend --max-spend-usd 0.150000 --out llm-mentions.json --dry-run" - }, - { - "title": "Live call after reviewing the dry-run", - "input": { - "target": [{ "domain": "clonesite.ai", "search_filter": "include", "search_scope": ["any"] }], - "location_code": 2840, - "language_code": "en", - "platform": "chat_gpt", - "limit": 20, - "order_by": ["ai_search_volume,desc"] - }, - "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.ai_optimization.llm_mentions.search.live --input @req.json --allow-spend --max-spend-usd 0.150000 --out llm-mentions.json" - } - ] - } - } - ] + "version": 1, + "provider": "dataforseo", + "manifestRevision": "2026-07-15.llm-mentions.2", + "reviewedAt": "2026-07-15T04:00:00.000Z", + "entries": [ + { + "path": "/v3/backlinks/bulk_ranks/live", + "method": "post", + "operationId": "BulkRanksLive", + "exposure": "executable", + "capability": { + "id": "dataforseo.backlinks.bulk_ranks.live", + "provider": "dataforseo", + "revision": "2", + "title": "Bulk backlink ranks", + "description": "Return DataForSEO backlink ranks for up to 1,000 targets in one live request.", + "adapterKey": "backlinks.bulk_ranks.live", + "effects": ["read", "spend"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["targets"], + "properties": { + "targets": { + "type": "array", + "description": "Domains, subdomains, or absolute URLs to rank.", + "minItems": 1, + "maxItems": 1000, + "items": { "type": "string", "minLength": 1 } + }, + "rank_scale": { + "type": "string", + "enum": ["one_hundred", "one_thousand"] + }, + "tag": { "type": "string", "maxLength": 255 } + } + }, + "cost": { + "currency": "USD", + "policyRevision": "dataforseo-backlinks-pricing-2026-07-14-v1", + "model": { + "type": "linear-items", + "baseMicros": 24000, + "perItemMicros": 36, + "itemsJsonPointer": "/targets", + "maxItems": 1000 + } + }, + "examples": [ + { + "title": "Safe preflight", + "input": { + "targets": ["clonesite.ai", "example.com"], + "rank_scale": "one_hundred", + "tag": "slice2-smoke" + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.bulk_ranks.live --input @req.json --allow-spend --max-spend-usd 0.024072 --out ranks.json --dry-run" + }, + { + "title": "Live call after reviewing the dry-run", + "input": { + "targets": ["clonesite.ai", "example.com"], + "rank_scale": "one_hundred", + "tag": "slice2-smoke" + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.bulk_ranks.live --input @req.json --allow-spend --max-spend-usd 0.024072 --out ranks.json" + } + ] + } + }, + { + "path": "/v3/backlinks/summary/live", + "method": "post", + "operationId": "SummaryLive", + "exposure": "executable", + "capability": { + "id": "dataforseo.backlinks.summary.live", + "provider": "dataforseo", + "revision": "1", + "title": "Backlink summary", + "description": "Return the reviewed live backlink summary for one domain, subdomain, or page.", + "adapterKey": "backlinks.summary.live", + "effects": ["read", "spend"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["target"], + "properties": { + "target": { "type": "string", "minLength": 1 }, + "include_subdomains": { "type": "boolean" }, + "exclude_internal_backlinks": { "type": "boolean" }, + "internal_list_limit": { + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + "backlinks_status_type": { + "type": "string", + "enum": ["all", "live", "lost"] + }, + "rank_scale": { + "type": "string", + "enum": ["one_hundred", "one_thousand"] + }, + "tag": { "type": "string", "maxLength": 255 } + } + }, + "cost": { + "currency": "USD", + "policyRevision": "dataforseo-backlinks-pricing-2026-07-14-v1", + "model": { "type": "fixed", "micros": 24036 } + }, + "examples": [ + { + "title": "Safe preflight", + "input": { + "target": "clonesite.ai", + "include_subdomains": true, + "backlinks_status_type": "live" + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.summary.live --input @req.json --allow-spend --max-spend-usd 0.024036 --out summary.json --dry-run" + }, + { + "title": "Live call after reviewing the dry-run", + "input": { + "target": "clonesite.ai", + "include_subdomains": true, + "backlinks_status_type": "live" + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.summary.live --input @req.json --allow-spend --max-spend-usd 0.024036 --out summary.json" + } + ] + } + }, + { + "path": "/v3/backlinks/referring_domains/live", + "method": "post", + "operationId": "ReferringDomainsLive", + "exposure": "executable", + "capability": { + "id": "dataforseo.backlinks.referring_domains.live", + "provider": "dataforseo", + "revision": "1", + "title": "Referring domains", + "description": "Return up to 100 reviewed referring-domain rows for one target.", + "adapterKey": "backlinks.referring_domains.live", + "effects": ["read", "spend"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["target", "limit"], + "properties": { + "target": { "type": "string", "minLength": 1 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, + "order_by": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "string", + "enum": [ + "rank,asc", + "rank,desc", + "backlinks,asc", + "backlinks,desc" + ] + } + }, + "include_subdomains": { "type": "boolean" }, + "exclude_internal_backlinks": { "type": "boolean" }, + "backlinks_status_type": { + "type": "string", + "enum": ["all", "live", "lost"] + }, + "rank_scale": { + "type": "string", + "enum": ["one_hundred", "one_thousand"] + }, + "tag": { "type": "string", "maxLength": 255 } + } + }, + "cost": { + "currency": "USD", + "policyRevision": "dataforseo-backlinks-pricing-2026-07-14-v1", + "model": { + "type": "linear-number", + "baseMicros": 24000, + "perUnitMicros": 36, + "valueJsonPointer": "/limit", + "maxValue": 100 + } + }, + "examples": [ + { + "title": "Safe preflight", + "input": { + "target": "clonesite.ai", + "limit": 20, + "order_by": ["rank,desc"], + "exclude_internal_backlinks": true + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.referring_domains.live --input @req.json --allow-spend --max-spend-usd 0.024720 --out referring-domains.json --dry-run" + }, + { + "title": "Live call after reviewing the dry-run", + "input": { + "target": "clonesite.ai", + "limit": 20, + "order_by": ["rank,desc"], + "exclude_internal_backlinks": true + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.backlinks.referring_domains.live --input @req.json --allow-spend --max-spend-usd 0.024720 --out referring-domains.json" + } + ] + } + }, + { + "path": "/v3/serp/google/organic/live/advanced", + "method": "post", + "operationId": "GoogleOrganicLiveAdvanced", + "exposure": "executable", + "capability": { + "id": "dataforseo.serp.google.organic.live.advanced", + "provider": "dataforseo", + "revision": "1", + "title": "Google Organic live advanced SERP", + "description": "Return one Google Organic live advanced SERP with a reviewed maximum depth of 10.", + "adapterKey": "serp.google.organic.live.advanced", + "effects": ["read", "spend"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "keyword", + "location_code", + "language_code", + "device", + "os", + "depth" + ], + "properties": { + "keyword": { + "type": "string", + "minLength": 1, + "maxLength": 700, + "not": { + "pattern": "(?:[Aa][Ll][Ll][Ii][Nn][Aa][Nn][Cc][Hh][Oo][Rr]|[Aa][Ll][Ll][Ii][Nn][Tt][Ee][Xx][Tt]|[Aa][Ll][Ll][Ii][Nn][Tt][Ii][Tt][Ll][Ee]|[Aa][Ll][Ll][Ii][Nn][Uu][Rr][Ll]|[Dd][Ee][Ff][Ii][Nn][Ee]|[Dd][Ee][Ff][Ii][Nn][Ii][Tt][Ii][Oo][Nn]|[Ff][Ii][Ll][Ee][Tt][Yy][Pp][Ee]|[Ii][Dd]|[Ii][Nn][Aa][Nn][Cc][Hh][Oo][Rr]|[Ii][Nn][Ff][Oo]|[Ii][Nn][Tt][Ee][Xx][Tt]|[Ii][Nn][Tt][Ii][Tt][Ll][Ee]|[Ii][Nn][Uu][Rr][Ll]|[Ll][Ii][Nn][Kk]|[Rr][Ee][Ll][Aa][Tt][Ee][Dd]|[Ss][Ii][Tt][Ee]|[Cc][Aa][Cc][Hh][Ee]):" + } + }, + "location_code": { "type": "integer", "minimum": 1 }, + "language_code": { + "type": "string", + "minLength": 2, + "maxLength": 10 + }, + "device": { "type": "string", "enum": ["desktop", "mobile"] }, + "os": { + "type": "string", + "enum": ["windows", "macos", "android", "ios"] + }, + "depth": { "type": "integer", "minimum": 1, "maximum": 10 }, + "tag": { "type": "string", "maxLength": 255 } + }, + "allOf": [ + { + "if": { "properties": { "device": { "const": "desktop" } } }, + "then": { + "properties": { "os": { "enum": ["windows", "macos"] } } + }, + "else": { "properties": { "os": { "enum": ["android", "ios"] } } } + } + ] + }, + "cost": { + "currency": "USD", + "policyRevision": "dataforseo-google-organic-live-pricing-2026-07-14-v1", + "model": { "type": "fixed", "micros": 2000 } + }, + "examples": [ + { + "title": "Safe preflight", + "input": { + "keyword": "website cloner", + "location_code": 2840, + "language_code": "en", + "device": "desktop", + "os": "windows", + "depth": 10 + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.serp.google.organic.live.advanced --input @req.json --allow-spend --max-spend-usd 0.002000 --out serp.json --dry-run" + }, + { + "title": "Live call after reviewing the dry-run", + "input": { + "keyword": "website cloner", + "location_code": 2840, + "language_code": "en", + "device": "desktop", + "os": "windows", + "depth": 10 + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.serp.google.organic.live.advanced --input @req.json --allow-spend --max-spend-usd 0.002000 --out serp.json" + } + ] + } + }, + { + "path": "/v3/ai_optimization/llm_mentions/search/live", + "method": "post", + "operationId": "LlmMentionsSearchLive", + "exposure": "executable", + "capability": { + "id": "dataforseo.ai_optimization.llm_mentions.search.live", + "provider": "dataforseo", + "revision": "1", + "title": "LLM mentions search", + "description": "Search reviewed ChatGPT or Google AI mention records for one domain in United States English results.", + "adapterKey": "ai_optimization.llm_mentions.search.live", + "effects": ["read", "spend"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "target", + "location_code", + "language_code", + "platform", + "limit" + ], + "properties": { + "target": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["domain"], + "properties": { + "domain": { + "type": "string", + "minLength": 1, + "maxLength": 63, + "pattern": "^(?!https?://)(?!www\\.)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,63}$" + }, + "search_filter": { "type": "string", "enum": ["include"] }, + "search_scope": { + "type": "array", + "minItems": 1, + "maxItems": 5, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "any", + "question", + "answer", + "brand_entities", + "fan_out_queries" + ] + } + }, + "include_subdomains": { "type": "boolean" } + } + } + }, + "location_code": { "type": "integer", "const": 2840 }, + "language_code": { "type": "string", "const": "en" }, + "platform": { "type": "string", "enum": ["chat_gpt", "google"] }, + "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, + "order_by": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "string", + "enum": [ + "ai_search_volume,asc", + "ai_search_volume,desc", + "last_response_at,asc", + "last_response_at,desc" + ] + } + }, + "tag": { "type": "string", "maxLength": 255 } + } + }, + "cost": { + "currency": "USD", + "policyRevision": "dataforseo-ai-optimization-llm-mentions-pricing-2026-07-15-v2", + "model": { "type": "fixed", "micros": 150000 } + }, + "examples": [ + { + "title": "Safe preflight", + "input": { + "target": [ + { + "domain": "clonesite.ai", + "search_filter": "include", + "search_scope": ["any"] + } + ], + "location_code": 2840, + "language_code": "en", + "platform": "chat_gpt", + "limit": 20, + "order_by": ["ai_search_volume,desc"] + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.ai_optimization.llm_mentions.search.live --input @req.json --allow-spend --max-spend-usd 0.150000 --out llm-mentions.json --dry-run" + }, + { + "title": "Live call after reviewing the dry-run", + "input": { + "target": [ + { + "domain": "clonesite.ai", + "search_filter": "include", + "search_scope": ["any"] + } + ], + "location_code": 2840, + "language_code": "en", + "platform": "chat_gpt", + "limit": 20, + "order_by": ["ai_search_volume,desc"] + }, + "command": "gkit --profile clonesite.ai dataforseo api call --capability-id dataforseo.ai_optimization.llm_mentions.search.live --input @req.json --allow-spend --max-spend-usd 0.150000 --out llm-mentions.json" + } + ] + } + } + ] } diff --git a/packages/gkit/policy/google-ads.reviewed.json b/packages/gkit/policy/google-ads.reviewed.json index c6e319c..1e63bf7 100644 --- a/packages/gkit/policy/google-ads.reviewed.json +++ b/packages/gkit/policy/google-ads.reviewed.json @@ -1,290 +1,321 @@ { - "version": 1, - "provider": "google-ads", - "manifestRevision": "2026-07-14.slice4.google-ads.1", - "reviewedAt": "2026-07-14T04:10:00.000Z", - "sourceChecksum": "sha256:202028d3abcb9e4681d35f3c28d06e6ced1eaac2ec57c56357c8ab5d522841d7", - "entries": [ - { - "path": "v24/customers:listAccessibleCustomers", - "method": "get", - "operationId": "googleads.customers.listAccessibleCustomers", - "exposure": "executable", - "capabilities": [ - { - "id": "google-ads.customers.list-accessible", - "provider": "google-ads", - "revision": "1", - "title": "List directly accessible Google Ads customers", - "description": "List customer resource names directly accessible to the authenticated service account.", - "adapterKey": "customers.list-accessible", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": {} - }, - "examples": [ - { - "title": "Safe preflight", - "input": {}, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.customers.list-accessible --input '{}' --out google-ads-customers.json --dry-run" - }, - { - "title": "Live read", - "input": {}, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.customers.list-accessible --input '{}' --out google-ads-customers.json" - } - ] - } - ] - }, - { - "path": "v24/googleAdsFields:search", - "method": "post", - "operationId": "googleads.googleAdsFields.search", - "exposure": "executable", - "capabilities": [ - { - "id": "google-ads.fields.describe", - "provider": "google-ads", - "revision": "1", - "title": "Describe one Google Ads field", - "description": "Read metadata for one exact Google Ads field name through the pinned field service.", - "adapterKey": "fields.describe", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["name"], - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "pattern": "^[a-z][a-z0-9_.]*$" - } - } - }, - "examples": [ - { - "title": "Describe campaign.id", - "input": { "name": "campaign.id" }, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.fields.describe --input '{\"name\":\"campaign.id\"}' --out campaign-id-field.json --dry-run" - } - ] - }, - { - "id": "google-ads.fields.search", - "provider": "google-ads", - "revision": "1", - "title": "Search Google Ads field metadata", - "description": "Run one provider-native GoogleAdsField query and explicitly paginate all result pages.", - "adapterKey": "fields.search", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["query"], - "properties": { - "query": { "type": "string", "minLength": 1, "maxLength": 20000 }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 10000 } - } - }, - "examples": [ - { - "title": "Search campaign fields", - "input": { "query": "SELECT name, category, data_type WHERE name LIKE 'campaign.%'" }, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.fields.search --input @fields.json --out fields-pages.json --dry-run" - } - ] - } - ] - }, - { - "path": "v24/customers/{+customerId}/googleAds:search", - "method": "post", - "operationId": "googleads.customers.googleAds.search", - "exposure": "executable", - "capabilities": [ - { - "id": "google-ads.query.gaql", - "provider": "google-ads", - "revision": "1", - "title": "Run a paginated GAQL read", - "description": "Run one provider-native GAQL query against the profile-bound customer and stream every REST page into one raw page bundle.", - "adapterKey": "query.gaql", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["query"], - "properties": { - "query": { - "type": "string", - "minLength": 1, - "maxLength": 200000, - "pattern": "^\\s*[Ss][Ee][Ll][Ee][Cc][Tt]\\b" - } - } - }, - "examples": [ - { - "title": "Read campaigns", - "input": { - "query": "SELECT campaign.id, campaign.name FROM campaign ORDER BY campaign.id LIMIT 25" - }, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.query.gaql --input @gaql.json --out gaql-pages.json --dry-run" - } - ] - } - ] - }, - { - "path": "v24/customers/{+customerId}:generateKeywordIdeas", - "method": "post", - "operationId": "googleads.customers.generateKeywordIdeas", - "exposure": "executable", - "capabilities": [ - { - "id": "google-ads.keyword-plan.generate-ideas", - "provider": "google-ads", - "revision": "1", - "title": "Generate keyword ideas", - "description": "Generate and explicitly paginate provider-native Keyword Planner ideas for the profile-bound customer.", - "adapterKey": "keyword-plan.generate-ideas", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { - "keywordSeed": { - "type": "object", - "additionalProperties": false, - "required": ["keywords"], - "properties": { - "keywords": { - "type": "array", - "minItems": 1, - "maxItems": 20, - "items": { "type": "string", "minLength": 1, "maxLength": 80 } - } - } - }, - "urlSeed": { - "type": "object", - "additionalProperties": false, - "required": ["url"], - "properties": { "url": { "type": "string", "minLength": 1, "maxLength": 2048 } } - }, - "keywordAndUrlSeed": { - "type": "object", - "additionalProperties": false, - "required": ["keywords", "url"], - "properties": { - "keywords": { - "type": "array", - "minItems": 1, - "maxItems": 20, - "items": { "type": "string", "minLength": 1, "maxLength": 80 } - }, - "url": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "geoTargetConstants": { - "type": "array", - "maxItems": 10, - "items": { "type": "string", "pattern": "^geoTargetConstants/[1-9][0-9]*$" } - }, - "language": { "type": "string", "pattern": "^languageConstants/[1-9][0-9]*$" }, - "keywordPlanNetwork": { "enum": ["GOOGLE_SEARCH", "GOOGLE_SEARCH_AND_PARTNERS"] }, - "includeAdultKeywords": { "type": "boolean" }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 10000 } - }, - "oneOf": [ - { "required": ["keywordSeed"], "properties": { "keywordSeed": {} } }, - { "required": ["urlSeed"], "properties": { "urlSeed": {} } }, - { - "required": ["keywordAndUrlSeed"], - "properties": { "keywordAndUrlSeed": {} } - } - ] - }, - "examples": [ - { - "title": "Generate US English ideas", - "input": { - "keywordSeed": { "keywords": ["website cloner"] }, - "geoTargetConstants": ["geoTargetConstants/2840"], - "language": "languageConstants/1000", - "keywordPlanNetwork": "GOOGLE_SEARCH" - }, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.keyword-plan.generate-ideas --input @ideas.json --out keyword-ideas-pages.json --dry-run" - } - ] - } - ] - }, - { - "path": "v24/customers/{+customerId}:generateKeywordHistoricalMetrics", - "method": "post", - "operationId": "googleads.customers.generateKeywordHistoricalMetrics", - "exposure": "executable", - "capabilities": [ - { - "id": "google-ads.keyword-plan.generate-historical-metrics", - "provider": "google-ads", - "revision": "1", - "title": "Generate keyword historical metrics", - "description": "Read provider-native Keyword Planner historical metrics for an explicit bounded keyword set.", - "adapterKey": "keyword-plan.generate-historical-metrics", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["keywords"], - "properties": { - "keywords": { - "type": "array", - "minItems": 1, - "maxItems": 100, - "items": { "type": "string", "minLength": 1, "maxLength": 80 } - }, - "geoTargetConstants": { - "type": "array", - "maxItems": 10, - "items": { "type": "string", "pattern": "^geoTargetConstants/[1-9][0-9]*$" } - }, - "language": { "type": "string", "pattern": "^languageConstants/[1-9][0-9]*$" }, - "keywordPlanNetwork": { "enum": ["GOOGLE_SEARCH", "GOOGLE_SEARCH_AND_PARTNERS"] }, - "historicalMetricsOptions": { - "type": "object", - "additionalProperties": false, - "properties": { "includeAverageCpc": { "type": "boolean" } } - } - } - }, - "examples": [ - { - "title": "Read US English metrics", - "input": { - "keywords": ["website cloner", "clone website"], - "geoTargetConstants": ["geoTargetConstants/2840"], - "language": "languageConstants/1000", - "keywordPlanNetwork": "GOOGLE_SEARCH", - "historicalMetricsOptions": { "includeAverageCpc": true } - }, - "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.keyword-plan.generate-historical-metrics --input @historical.json --out keyword-history-pages.json --dry-run" - } - ] - } - ] - } - ] + "version": 1, + "provider": "google-ads", + "manifestRevision": "2026-07-14.slice4.google-ads.1", + "reviewedAt": "2026-07-14T04:10:00.000Z", + "sourceChecksum": "sha256:202028d3abcb9e4681d35f3c28d06e6ced1eaac2ec57c56357c8ab5d522841d7", + "entries": [ + { + "path": "v24/customers:listAccessibleCustomers", + "method": "get", + "operationId": "googleads.customers.listAccessibleCustomers", + "exposure": "executable", + "capabilities": [ + { + "id": "google-ads.customers.list-accessible", + "provider": "google-ads", + "revision": "1", + "title": "List directly accessible Google Ads customers", + "description": "List customer resource names directly accessible to the authenticated service account.", + "adapterKey": "customers.list-accessible", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "examples": [ + { + "title": "Safe preflight", + "input": {}, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.customers.list-accessible --input '{}' --out google-ads-customers.json --dry-run" + }, + { + "title": "Live read", + "input": {}, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.customers.list-accessible --input '{}' --out google-ads-customers.json" + } + ] + } + ] + }, + { + "path": "v24/googleAdsFields:search", + "method": "post", + "operationId": "googleads.googleAdsFields.search", + "exposure": "executable", + "capabilities": [ + { + "id": "google-ads.fields.describe", + "provider": "google-ads", + "revision": "1", + "title": "Describe one Google Ads field", + "description": "Read metadata for one exact Google Ads field name through the pinned field service.", + "adapterKey": "fields.describe", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^[a-z][a-z0-9_.]*$" + } + } + }, + "examples": [ + { + "title": "Describe campaign.id", + "input": { "name": "campaign.id" }, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.fields.describe --input '{\"name\":\"campaign.id\"}' --out campaign-id-field.json --dry-run" + } + ] + }, + { + "id": "google-ads.fields.search", + "provider": "google-ads", + "revision": "1", + "title": "Search Google Ads field metadata", + "description": "Run one provider-native GoogleAdsField query and explicitly paginate all result pages.", + "adapterKey": "fields.search", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["query"], + "properties": { + "query": { "type": "string", "minLength": 1, "maxLength": 20000 }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 10000 } + } + }, + "examples": [ + { + "title": "Search campaign fields", + "input": { + "query": "SELECT name, category, data_type WHERE name LIKE 'campaign.%'" + }, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.fields.search --input @fields.json --out fields-pages.json --dry-run" + } + ] + } + ] + }, + { + "path": "v24/customers/{+customerId}/googleAds:search", + "method": "post", + "operationId": "googleads.customers.googleAds.search", + "exposure": "executable", + "capabilities": [ + { + "id": "google-ads.query.gaql", + "provider": "google-ads", + "revision": "1", + "title": "Run a paginated GAQL read", + "description": "Run one provider-native GAQL query against the profile-bound customer and stream every REST page into one raw page bundle.", + "adapterKey": "query.gaql", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["query"], + "properties": { + "query": { + "type": "string", + "minLength": 1, + "maxLength": 200000, + "pattern": "^\\s*[Ss][Ee][Ll][Ee][Cc][Tt]\\b" + } + } + }, + "examples": [ + { + "title": "Read campaigns", + "input": { + "query": "SELECT campaign.id, campaign.name FROM campaign ORDER BY campaign.id LIMIT 25" + }, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.query.gaql --input @gaql.json --out gaql-pages.json --dry-run" + } + ] + } + ] + }, + { + "path": "v24/customers/{+customerId}:generateKeywordIdeas", + "method": "post", + "operationId": "googleads.customers.generateKeywordIdeas", + "exposure": "executable", + "capabilities": [ + { + "id": "google-ads.keyword-plan.generate-ideas", + "provider": "google-ads", + "revision": "1", + "title": "Generate keyword ideas", + "description": "Generate and explicitly paginate provider-native Keyword Planner ideas for the profile-bound customer.", + "adapterKey": "keyword-plan.generate-ideas", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "keywordSeed": { + "type": "object", + "additionalProperties": false, + "required": ["keywords"], + "properties": { + "keywords": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + } + } + }, + "urlSeed": { + "type": "object", + "additionalProperties": false, + "required": ["url"], + "properties": { + "url": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "keywordAndUrlSeed": { + "type": "object", + "additionalProperties": false, + "required": ["keywords", "url"], + "properties": { + "keywords": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 80 + } + }, + "url": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "geoTargetConstants": { + "type": "array", + "maxItems": 10, + "items": { + "type": "string", + "pattern": "^geoTargetConstants/[1-9][0-9]*$" + } + }, + "language": { + "type": "string", + "pattern": "^languageConstants/[1-9][0-9]*$" + }, + "keywordPlanNetwork": { + "enum": ["GOOGLE_SEARCH", "GOOGLE_SEARCH_AND_PARTNERS"] + }, + "includeAdultKeywords": { "type": "boolean" }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 10000 } + }, + "oneOf": [ + { + "required": ["keywordSeed"], + "properties": { "keywordSeed": {} } + }, + { "required": ["urlSeed"], "properties": { "urlSeed": {} } }, + { + "required": ["keywordAndUrlSeed"], + "properties": { "keywordAndUrlSeed": {} } + } + ] + }, + "examples": [ + { + "title": "Generate US English ideas", + "input": { + "keywordSeed": { "keywords": ["website cloner"] }, + "geoTargetConstants": ["geoTargetConstants/2840"], + "language": "languageConstants/1000", + "keywordPlanNetwork": "GOOGLE_SEARCH" + }, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.keyword-plan.generate-ideas --input @ideas.json --out keyword-ideas-pages.json --dry-run" + } + ] + } + ] + }, + { + "path": "v24/customers/{+customerId}:generateKeywordHistoricalMetrics", + "method": "post", + "operationId": "googleads.customers.generateKeywordHistoricalMetrics", + "exposure": "executable", + "capabilities": [ + { + "id": "google-ads.keyword-plan.generate-historical-metrics", + "provider": "google-ads", + "revision": "1", + "title": "Generate keyword historical metrics", + "description": "Read provider-native Keyword Planner historical metrics for an explicit bounded keyword set.", + "adapterKey": "keyword-plan.generate-historical-metrics", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["keywords"], + "properties": { + "keywords": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { "type": "string", "minLength": 1, "maxLength": 80 } + }, + "geoTargetConstants": { + "type": "array", + "maxItems": 10, + "items": { + "type": "string", + "pattern": "^geoTargetConstants/[1-9][0-9]*$" + } + }, + "language": { + "type": "string", + "pattern": "^languageConstants/[1-9][0-9]*$" + }, + "keywordPlanNetwork": { + "enum": ["GOOGLE_SEARCH", "GOOGLE_SEARCH_AND_PARTNERS"] + }, + "historicalMetricsOptions": { + "type": "object", + "additionalProperties": false, + "properties": { "includeAverageCpc": { "type": "boolean" } } + } + } + }, + "examples": [ + { + "title": "Read US English metrics", + "input": { + "keywords": ["website cloner", "clone website"], + "geoTargetConstants": ["geoTargetConstants/2840"], + "language": "languageConstants/1000", + "keywordPlanNetwork": "GOOGLE_SEARCH", + "historicalMetricsOptions": { "includeAverageCpc": true } + }, + "command": "gkit --profile openclaw-web google-ads api call --capability-id google-ads.keyword-plan.generate-historical-metrics --input @historical.json --out keyword-history-pages.json --dry-run" + } + ] + } + ] + } + ] } diff --git a/packages/gkit/policy/gsc.reviewed.json b/packages/gkit/policy/gsc.reviewed.json index 1762bb1..a606556 100644 --- a/packages/gkit/policy/gsc.reviewed.json +++ b/packages/gkit/policy/gsc.reviewed.json @@ -1,254 +1,291 @@ { - "version": 1, - "provider": "gsc", - "manifestRevision": "2026-10-21.gsc.3", - "reviewedAt": "2026-10-21T00:00:00.000Z", - "entries": [ - { - "path": "/webmasters/v3/sites", - "method": "get", - "operationId": "sites.list", - "exposure": "executable", - "capability": { - "id": "gsc.properties.list", - "provider": "gsc", - "revision": "1", - "adapterKey": "properties.list", - "title": "List accessible Search Console properties", - "description": "List Search Console properties accessible to the profile-bound service account.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": {} - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.properties.list --input '{}' --out gsc-properties.json --dry-run" - } - ] - } - }, - { - "path": "/webmasters/v3/sites/{siteUrl}", - "method": "get", - "operationId": "sites.get", - "exposure": "inventory", - "reason": "The legacy CLI does not expose a distinct property-get workflow; properties.list carries the reviewed inventory use case." - }, - { - "path": "/webmasters/v3/sites/{siteUrl}", - "method": "put", - "operationId": "sites.add", - "exposure": "inventory", - "reason": "Write operation is outside Slice 4." - }, - { - "path": "/webmasters/v3/sites/{siteUrl}", - "method": "delete", - "operationId": "sites.delete", - "exposure": "inventory", - "reason": "Destructive operation is outside Slice 4." - }, - { - "path": "/webmasters/v3/sites/{siteUrl}/searchAnalytics/query", - "method": "post", - "operationId": "searchanalytics.query", - "exposure": "executable", - "capability": { - "id": "gsc.search-analytics.query", - "provider": "gsc", - "revision": "1", - "adapterKey": "search-analytics.query", - "title": "Query Search Console performance", - "description": "Run one provider-native Search Analytics read for the profile-bound or explicitly selected property.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["startDate", "endDate"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "startDate": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, - "endDate": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, - "dimensions": { - "type": "array", - "maxItems": 8, - "uniqueItems": true, - "items": { - "enum": ["country", "date", "device", "hour", "page", "query", "searchAppearance"] - } - }, - "type": { "enum": ["discover", "googleNews", "image", "news", "video", "web"] }, - "dataState": { "enum": ["all", "final", "hourly_all"] }, - "aggregationType": { "enum": ["auto", "byNewsShowcasePanel", "byPage", "byProperty"] }, - "rowLimit": { "type": "integer", "minimum": 1, "maximum": 25000 }, - "startRow": { "type": "integer", "minimum": 0, "maximum": 100000000 }, - "dimensionFilterGroups": { - "type": "array", - "maxItems": 10, - "items": { "type": "object" } - } - } - }, - "examples": [ - { - "input": { - "startDate": "2026-07-01", - "endDate": "2026-07-07", - "dimensions": ["query"], - "rowLimit": 25 - }, - "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.search-analytics.query --input @request.json --out gsc-search.json --dry-run" - } - ] - } - }, - { - "path": "/webmasters/v3/sites/{siteUrl}/sitemaps", - "method": "get", - "operationId": "sitemaps.list", - "exposure": "executable", - "capability": { - "id": "gsc.sitemaps.list", - "provider": "gsc", - "revision": "1", - "adapterKey": "sitemaps.list", - "title": "List Search Console sitemaps", - "description": "List sitemaps for one Search Console property.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "sitemapIndex": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "input": {}, - "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.sitemaps.list --input '{}' --out gsc-sitemaps.json --dry-run" - } - ] - } - }, - { - "path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}", - "method": "get", - "operationId": "sitemaps.get", - "exposure": "executable", - "capability": { - "id": "gsc.sitemaps.get", - "provider": "gsc", - "revision": "2", - "adapterKey": "sitemaps.get", - "title": "Read one Search Console sitemap", - "description": "Read details for one exact sitemap URL under one Search Console property.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["feedpath"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "feedpath": { "type": "string", "minLength": 1, "maxLength": 2048 } - } - }, - "examples": [ - { - "title": "Read a sitemap returned by gsc.sitemaps.list for openclaw-web", - "input": { "feedpath": "https://openclawai.io/sitemap-index.xml" }, - "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.sitemaps.get --input @request.json --out gsc-sitemap.json --dry-run" - } - ] - } - }, - { - "path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}", - "method": "put", - "operationId": "sitemaps.submit", - "exposure": "executable", - "capability": { - "id": "gsc.sitemaps.submit", - "provider": "gsc", - "revision": "1", - "adapterKey": "sitemaps.submit", - "title": "Submit one sitemap to Search Console", - "description": "Submit or resubmit one exact sitemap URL for one Search Console property. Changes provider state, so it requires --allow-write, and the service account needs Full or Owner permission on the property; a Restricted user is rejected with AUTH_FAILED. Google returns an empty body, so the artifact is a gkit submission receipt with siteUrl, feedpath, and httpStatus.", - "effects": ["write"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["feedpath"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "feedpath": { - "type": "string", - "minLength": 1, - "maxLength": 2048, - "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://[^\\s/?#]+" - } - } - }, - "examples": [ - { - "title": "Submit the openclaw-web sitemap index", - "input": { "feedpath": "https://openclawai.io/sitemap-index.xml" }, - "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.sitemaps.submit --input @request.json --allow-write --out gsc-sitemap-submit.json --dry-run" - } - ] - } - }, - { - "path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}", - "method": "delete", - "operationId": "sitemaps.delete", - "exposure": "inventory", - "reason": "Destructive operation is outside Slice 4." - }, - { - "path": "/v1/urlInspection/index:inspect", - "method": "post", - "operationId": "urlInspection.index.inspect", - "exposure": "executable", - "capability": { - "id": "gsc.url-inspection.inspect", - "provider": "gsc", - "revision": "2", - "adapterKey": "url-inspection.inspect", - "title": "Inspect one URL in Google's index", - "description": "Read Google's indexed URL inspection result for one URL under the selected property.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["inspectionUrl"], - "properties": { - "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "inspectionUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, - "languageCode": { - "type": "string", - "pattern": "^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$", - "maxLength": 35 - } - } - }, - "examples": [ - { - "title": "Inspect a URL under the openclaw-web Search Console property", - "input": { "inspectionUrl": "https://openclawai.io/" }, - "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.url-inspection.inspect --input @request.json --out gsc-inspection.json --dry-run" - } - ] - } - } - ] + "version": 1, + "provider": "gsc", + "manifestRevision": "2026-10-21.gsc.3", + "reviewedAt": "2026-10-21T00:00:00.000Z", + "entries": [ + { + "path": "/webmasters/v3/sites", + "method": "get", + "operationId": "sites.list", + "exposure": "executable", + "capability": { + "id": "gsc.properties.list", + "provider": "gsc", + "revision": "1", + "adapterKey": "properties.list", + "title": "List accessible Search Console properties", + "description": "List Search Console properties accessible to the profile-bound service account.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.properties.list --input '{}' --out gsc-properties.json --dry-run" + } + ] + } + }, + { + "path": "/webmasters/v3/sites/{siteUrl}", + "method": "get", + "operationId": "sites.get", + "exposure": "inventory", + "reason": "The legacy CLI does not expose a distinct property-get workflow; properties.list carries the reviewed inventory use case." + }, + { + "path": "/webmasters/v3/sites/{siteUrl}", + "method": "put", + "operationId": "sites.add", + "exposure": "inventory", + "reason": "Write operation is outside Slice 4." + }, + { + "path": "/webmasters/v3/sites/{siteUrl}", + "method": "delete", + "operationId": "sites.delete", + "exposure": "inventory", + "reason": "Destructive operation is outside Slice 4." + }, + { + "path": "/webmasters/v3/sites/{siteUrl}/searchAnalytics/query", + "method": "post", + "operationId": "searchanalytics.query", + "exposure": "executable", + "capability": { + "id": "gsc.search-analytics.query", + "provider": "gsc", + "revision": "1", + "adapterKey": "search-analytics.query", + "title": "Query Search Console performance", + "description": "Run one provider-native Search Analytics read for the profile-bound or explicitly selected property.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["startDate", "endDate"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "startDate": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" + }, + "endDate": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" + }, + "dimensions": { + "type": "array", + "maxItems": 8, + "uniqueItems": true, + "items": { + "enum": [ + "country", + "date", + "device", + "hour", + "page", + "query", + "searchAppearance" + ] + } + }, + "type": { + "enum": [ + "discover", + "googleNews", + "image", + "news", + "video", + "web" + ] + }, + "dataState": { "enum": ["all", "final", "hourly_all"] }, + "aggregationType": { + "enum": ["auto", "byNewsShowcasePanel", "byPage", "byProperty"] + }, + "rowLimit": { "type": "integer", "minimum": 1, "maximum": 25000 }, + "startRow": { + "type": "integer", + "minimum": 0, + "maximum": 100000000 + }, + "dimensionFilterGroups": { + "type": "array", + "maxItems": 10, + "items": { "type": "object" } + } + } + }, + "examples": [ + { + "input": { + "startDate": "2026-07-01", + "endDate": "2026-07-07", + "dimensions": ["query"], + "rowLimit": 25 + }, + "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.search-analytics.query --input @request.json --out gsc-search.json --dry-run" + } + ] + } + }, + { + "path": "/webmasters/v3/sites/{siteUrl}/sitemaps", + "method": "get", + "operationId": "sitemaps.list", + "exposure": "executable", + "capability": { + "id": "gsc.sitemaps.list", + "provider": "gsc", + "revision": "1", + "adapterKey": "sitemaps.list", + "title": "List Search Console sitemaps", + "description": "List sitemaps for one Search Console property.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "sitemapIndex": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "examples": [ + { + "input": {}, + "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.sitemaps.list --input '{}' --out gsc-sitemaps.json --dry-run" + } + ] + } + }, + { + "path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}", + "method": "get", + "operationId": "sitemaps.get", + "exposure": "executable", + "capability": { + "id": "gsc.sitemaps.get", + "provider": "gsc", + "revision": "2", + "adapterKey": "sitemaps.get", + "title": "Read one Search Console sitemap", + "description": "Read details for one exact sitemap URL under one Search Console property.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["feedpath"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "feedpath": { "type": "string", "minLength": 1, "maxLength": 2048 } + } + }, + "examples": [ + { + "title": "Read a sitemap returned by gsc.sitemaps.list for openclaw-web", + "input": { "feedpath": "https://openclawai.io/sitemap-index.xml" }, + "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.sitemaps.get --input @request.json --out gsc-sitemap.json --dry-run" + } + ] + } + }, + { + "path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}", + "method": "put", + "operationId": "sitemaps.submit", + "exposure": "executable", + "capability": { + "id": "gsc.sitemaps.submit", + "provider": "gsc", + "revision": "1", + "adapterKey": "sitemaps.submit", + "title": "Submit one sitemap to Search Console", + "description": "Submit or resubmit one exact sitemap URL for one Search Console property. Changes provider state, so it requires --allow-write, and the service account needs Full or Owner permission on the property; a Restricted user is rejected with AUTH_FAILED. Google returns an empty body, so the artifact is a gkit submission receipt with siteUrl, feedpath, and httpStatus.", + "effects": ["write"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["feedpath"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "feedpath": { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://[^\\s/?#]+" + } + } + }, + "examples": [ + { + "title": "Submit the openclaw-web sitemap index", + "input": { "feedpath": "https://openclawai.io/sitemap-index.xml" }, + "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.sitemaps.submit --input @request.json --allow-write --out gsc-sitemap-submit.json --dry-run" + } + ] + } + }, + { + "path": "/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}", + "method": "delete", + "operationId": "sitemaps.delete", + "exposure": "inventory", + "reason": "Destructive operation is outside Slice 4." + }, + { + "path": "/v1/urlInspection/index:inspect", + "method": "post", + "operationId": "urlInspection.index.inspect", + "exposure": "executable", + "capability": { + "id": "gsc.url-inspection.inspect", + "provider": "gsc", + "revision": "2", + "adapterKey": "url-inspection.inspect", + "title": "Inspect one URL in Google's index", + "description": "Read Google's indexed URL inspection result for one URL under the selected property.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["inspectionUrl"], + "properties": { + "siteUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, + "inspectionUrl": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "languageCode": { + "type": "string", + "pattern": "^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$", + "maxLength": 35 + } + } + }, + "examples": [ + { + "title": "Inspect a URL under the openclaw-web Search Console property", + "input": { "inspectionUrl": "https://openclawai.io/" }, + "command": "gkit --profile openclaw-web gsc api call --capability-id gsc.url-inspection.inspect --input @request.json --out gsc-inspection.json --dry-run" + } + ] + } + } + ] } diff --git a/packages/gkit/policy/hubspot.reviewed.json b/packages/gkit/policy/hubspot.reviewed.json index c04ff33..80c5798 100644 --- a/packages/gkit/policy/hubspot.reviewed.json +++ b/packages/gkit/policy/hubspot.reviewed.json @@ -1,300 +1,441 @@ { - "version": 1, - "provider": "hubspot", - "manifestRevision": "2026-08-30.hubspot.read-v1.1", - "reviewedAt": "2026-08-30T05:00:00.000Z", - "entries": [ - { - "path": "/crm/properties/2026-03/{objectType}", - "method": "get", - "operationId": "crm.properties.list", - "exposure": "executable", - "capability": { - "id": "hubspot.crm.properties.list", - "provider": "hubspot", - "revision": "1", - "adapterKey": "crm.properties.list", - "title": "List CRM property metadata", - "description": "List non-sensitive property definitions for one reviewed CRM object type.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["objectType"], - "properties": { "objectType": { "enum": ["companies", "contacts", "deals", "tickets"] } } - }, - "examples": [{ - "input": { "objectType": "contacts" }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.properties.list --input '{\"objectType\":\"contacts\"}' --out hubspot-contact-properties.json --dry-run" - }] - } - }, - { - "path": "/crm/properties/2026-03/{objectType}", - "method": "post", - "operationId": "crm.properties.create", - "exposure": "inventory", - "reason": "Mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/objects/2026-03/{objectType}", - "method": "get", - "operationId": "crm.objects.list", - "exposure": "executable", - "capability": { - "id": "hubspot.crm.objects.list", - "provider": "hubspot", - "revision": "1", - "adapterKey": "crm.objects.list", - "title": "List CRM records", - "description": "List bounded records for one reviewed CRM object type and reviewed properties.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["objectType", "properties", "limit"], - "properties": { - "objectType": { "enum": ["companies", "contacts", "deals", "tickets"] }, - "properties": { "type": "array", "minItems": 1, "uniqueItems": true, "maxItems": 50, "items": { "$ref": "#/definitions/property" } }, - "archived": { "type": "boolean" }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, - "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } - }, - "definitions": { "property": { "enum": ["amount", "city", "closedate", "content", "country", "createdate", "dealname", "dealstage", "domain", "email", "firstname", "hs_analytics_source", "hs_analytics_source_data_1", "hs_analytics_source_data_2", "hs_lastmodifieddate", "hs_lead_status", "hs_object_id", "hs_pipeline", "hs_pipeline_stage", "hs_ticket_category", "hs_ticket_priority", "hubspot_owner_id", "industry", "lastmodifieddate", "lastname", "lifecyclestage", "name", "numberofemployees", "pipeline", "state", "subject"] } } - }, - "examples": [{ - "input": { "objectType": "contacts", "properties": ["email", "lifecyclestage"], "limit": 100 }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.objects.list --input @request.json --out hubspot-contacts.json --dry-run" - }] - } - }, - { - "path": "/crm/objects/2026-03/{objectType}", - "method": "post", - "operationId": "crm.objects.create", - "exposure": "inventory", - "reason": "Mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/objects/2026-03/{objectType}/search", - "method": "post", - "operationId": "crm.objects.search", - "exposure": "executable", - "capability": { - "id": "hubspot.crm.objects.search", - "provider": "hubspot", - "revision": "1", - "adapterKey": "crm.objects.search", - "title": "Search CRM records", - "description": "Run one bounded HubSpot CRM Search POST over reviewed objects, properties, filters, and sorts.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["objectType", "properties", "limit"], - "properties": { - "objectType": { "enum": ["companies", "contacts", "deals", "tickets"] }, - "properties": { "type": "array", "minItems": 1, "uniqueItems": true, "maxItems": 50, "items": { "type": "string", "minLength": 1, "maxLength": 100 } }, - "query": { "type": "string", "minLength": 1, "maxLength": 3000 }, - "filterGroups": { "type": "array", "maxItems": 5, "items": { "type": "object" } }, - "sorts": { "type": "array", "maxItems": 1, "items": { "type": "object" } }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 200 }, - "limit": { "type": "integer", "minimum": 1, "maximum": 10000 } - } - }, - "examples": [{ - "input": { "objectType": "contacts", "properties": ["email", "lifecyclestage"], "filterGroups": [{ "filters": [{ "propertyName": "lifecyclestage", "operator": "EQ", "value": "customer" }] }], "limit": 200 }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.objects.search --input @request.json --out hubspot-contact-search.json --dry-run" - }] - } - }, - { - "path": "/crm/objects/2026-03/{objectType}/{recordId}", - "method": "patch", - "operationId": "crm.objects.update", - "exposure": "inventory", - "reason": "Mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/objects/2026-03/{objectType}/{recordId}", - "method": "delete", - "operationId": "crm.objects.archive", - "exposure": "inventory", - "reason": "Destructive mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/objects/2026-03/{fromObjectType}/{objectId}/associations/{toObjectType}", - "method": "get", - "operationId": "crm.associations.list", - "exposure": "executable", - "capability": { - "id": "hubspot.crm.associations.list", - "provider": "hubspot", - "revision": "1", - "adapterKey": "crm.associations.list", - "title": "List record associations", - "description": "List bounded associations from one reviewed CRM record to one reviewed object type.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["fromObjectType", "objectId", "toObjectType", "limit"], - "properties": { - "fromObjectType": { "enum": ["companies", "contacts", "deals", "tickets"] }, - "objectId": { "type": "string", "pattern": "^[1-9]\\d{0,19}$" }, - "toObjectType": { "enum": ["companies", "contacts", "deals", "tickets"] }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, - "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } - } - }, - "examples": [{ - "input": { "fromObjectType": "contacts", "objectId": "123", "toObjectType": "companies", "limit": 100 }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.associations.list --input @request.json --out hubspot-associations.json --dry-run" - }] - } - }, - { - "path": "/crm/objects/2026-03/{fromObjectType}/{objectId}/associations/default/{toObjectType}/{toObjectId}", - "method": "put", - "operationId": "crm.associations.create", - "exposure": "inventory", - "reason": "Mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/objects/2026-03/{fromObjectType}/{objectId}/associations/{toObjectType}/{toObjectId}", - "method": "delete", - "operationId": "crm.associations.delete", - "exposure": "inventory", - "reason": "Destructive mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/events/event-occurrences/2026-03", - "method": "get", - "operationId": "events.occurrences.list", - "exposure": "executable", - "capability": { - "id": "hubspot.events.occurrences.list", - "provider": "hubspot", - "revision": "1", - "adapterKey": "events.occurrences.list", - "title": "List event occurrences", - "description": "List bounded HubSpot event occurrences within an explicit time window.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["occurredAfter", "occurredBefore", "properties", "limit"], - "properties": { - "occurredAfter": { "type": "string", "format": "date-time", "maxLength": 64 }, - "occurredBefore": { "type": "string", "format": "date-time", "maxLength": 64 }, - "eventType": { "type": "string", "minLength": 1, "maxLength": 200 }, - "objectType": { "enum": ["company", "contact", "deal", "ticket"] }, - "objectId": { "type": "string", "pattern": "^[1-9]\\d{0,19}$" }, - "properties": { - "type": "array", - "minItems": 1, - "maxItems": 20, - "uniqueItems": true, - "items": { "enum": ["hs_browser", "hs_city", "hs_content_type", "hs_country", "hs_device_name", "hs_device_type", "hs_page_title", "hs_referrer", "hs_touchpoint_source", "hs_url", "hs_utm_campaign", "hs_utm_medium", "hs_utm_source"] } - }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, - "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } - } - }, - "examples": [{ - "input": { "occurredAfter": "2026-08-01T00:00:00Z", "occurredBefore": "2026-08-02T00:00:00Z", "eventType": "e_visited_page", "properties": ["hs_url", "hs_page_title"], "limit": 100 }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.events.occurrences.list --input @request.json --out hubspot-events.json --dry-run" - }] - } - }, - { - "path": "/events/2026-03/send", - "method": "post", - "operationId": "events.occurrences.send", - "exposure": "inventory", - "reason": "Event sending is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/pipelines/2026-03/{objectType}", - "method": "get", - "operationId": "crm.pipelines.list", - "exposure": "executable", - "capability": { - "id": "hubspot.crm.pipelines.list", - "provider": "hubspot", - "revision": "1", - "adapterKey": "crm.pipelines.list", - "title": "List CRM pipelines", - "description": "List pipelines and stages for deals or tickets.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["objectType"], - "properties": { "objectType": { "enum": ["deals", "tickets"] } } - }, - "examples": [{ - "input": { "objectType": "deals" }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.pipelines.list --input '{\"objectType\":\"deals\"}' --out hubspot-deal-pipelines.json --dry-run" - }] - } - }, - { - "path": "/crm/pipelines/2026-03/{objectType}", - "method": "post", - "operationId": "crm.pipelines.create", - "exposure": "inventory", - "reason": "Mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/pipelines/2026-03/{objectType}/{pipelineId}", - "method": "delete", - "operationId": "crm.pipelines.delete", - "exposure": "inventory", - "reason": "Destructive mutation is outside the read-only HubSpot V1 provider." - }, - { - "path": "/crm/owners/2026-03", - "method": "get", - "operationId": "crm.owners.list", - "exposure": "executable", - "capability": { - "id": "hubspot.crm.owners.list", - "provider": "hubspot", - "revision": "1", - "adapterKey": "crm.owners.list", - "title": "List CRM owners", - "description": "List bounded active or archived HubSpot owners for attribution joins.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["limit"], - "properties": { - "archived": { "type": "boolean" }, - "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, - "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } - } - }, - "examples": [{ - "input": { "limit": 100 }, - "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.owners.list --input '{\"limit\":100}' --out hubspot-owners.json --dry-run" - }] - } - }, - { - "path": "/crm/imports/2026-03/imports", - "method": "post", - "operationId": "crm.imports.create", - "exposure": "inventory", - "reason": "Imports are mutations and are outside the read-only HubSpot V1 provider." - } - ] + "version": 1, + "provider": "hubspot", + "manifestRevision": "2026-08-30.hubspot.read-v1.1", + "reviewedAt": "2026-08-30T05:00:00.000Z", + "entries": [ + { + "path": "/crm/properties/2026-03/{objectType}", + "method": "get", + "operationId": "crm.properties.list", + "exposure": "executable", + "capability": { + "id": "hubspot.crm.properties.list", + "provider": "hubspot", + "revision": "1", + "adapterKey": "crm.properties.list", + "title": "List CRM property metadata", + "description": "List non-sensitive property definitions for one reviewed CRM object type.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["objectType"], + "properties": { + "objectType": { + "enum": ["companies", "contacts", "deals", "tickets"] + } + } + }, + "examples": [ + { + "input": { "objectType": "contacts" }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.properties.list --input '{\"objectType\":\"contacts\"}' --out hubspot-contact-properties.json --dry-run" + } + ] + } + }, + { + "path": "/crm/properties/2026-03/{objectType}", + "method": "post", + "operationId": "crm.properties.create", + "exposure": "inventory", + "reason": "Mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/objects/2026-03/{objectType}", + "method": "get", + "operationId": "crm.objects.list", + "exposure": "executable", + "capability": { + "id": "hubspot.crm.objects.list", + "provider": "hubspot", + "revision": "1", + "adapterKey": "crm.objects.list", + "title": "List CRM records", + "description": "List bounded records for one reviewed CRM object type and reviewed properties.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["objectType", "properties", "limit"], + "properties": { + "objectType": { + "enum": ["companies", "contacts", "deals", "tickets"] + }, + "properties": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "maxItems": 50, + "items": { "$ref": "#/definitions/property" } + }, + "archived": { "type": "boolean" }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } + }, + "definitions": { + "property": { + "enum": [ + "amount", + "city", + "closedate", + "content", + "country", + "createdate", + "dealname", + "dealstage", + "domain", + "email", + "firstname", + "hs_analytics_source", + "hs_analytics_source_data_1", + "hs_analytics_source_data_2", + "hs_lastmodifieddate", + "hs_lead_status", + "hs_object_id", + "hs_pipeline", + "hs_pipeline_stage", + "hs_ticket_category", + "hs_ticket_priority", + "hubspot_owner_id", + "industry", + "lastmodifieddate", + "lastname", + "lifecyclestage", + "name", + "numberofemployees", + "pipeline", + "state", + "subject" + ] + } + } + }, + "examples": [ + { + "input": { + "objectType": "contacts", + "properties": ["email", "lifecyclestage"], + "limit": 100 + }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.objects.list --input @request.json --out hubspot-contacts.json --dry-run" + } + ] + } + }, + { + "path": "/crm/objects/2026-03/{objectType}", + "method": "post", + "operationId": "crm.objects.create", + "exposure": "inventory", + "reason": "Mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/objects/2026-03/{objectType}/search", + "method": "post", + "operationId": "crm.objects.search", + "exposure": "executable", + "capability": { + "id": "hubspot.crm.objects.search", + "provider": "hubspot", + "revision": "1", + "adapterKey": "crm.objects.search", + "title": "Search CRM records", + "description": "Run one bounded HubSpot CRM Search POST over reviewed objects, properties, filters, and sorts.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["objectType", "properties", "limit"], + "properties": { + "objectType": { + "enum": ["companies", "contacts", "deals", "tickets"] + }, + "properties": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "maxItems": 50, + "items": { "type": "string", "minLength": 1, "maxLength": 100 } + }, + "query": { "type": "string", "minLength": 1, "maxLength": 3000 }, + "filterGroups": { + "type": "array", + "maxItems": 5, + "items": { "type": "object" } + }, + "sorts": { + "type": "array", + "maxItems": 1, + "items": { "type": "object" } + }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 200 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 10000 } + } + }, + "examples": [ + { + "input": { + "objectType": "contacts", + "properties": ["email", "lifecyclestage"], + "filterGroups": [ + { + "filters": [ + { + "propertyName": "lifecyclestage", + "operator": "EQ", + "value": "customer" + } + ] + } + ], + "limit": 200 + }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.objects.search --input @request.json --out hubspot-contact-search.json --dry-run" + } + ] + } + }, + { + "path": "/crm/objects/2026-03/{objectType}/{recordId}", + "method": "patch", + "operationId": "crm.objects.update", + "exposure": "inventory", + "reason": "Mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/objects/2026-03/{objectType}/{recordId}", + "method": "delete", + "operationId": "crm.objects.archive", + "exposure": "inventory", + "reason": "Destructive mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/objects/2026-03/{fromObjectType}/{objectId}/associations/{toObjectType}", + "method": "get", + "operationId": "crm.associations.list", + "exposure": "executable", + "capability": { + "id": "hubspot.crm.associations.list", + "provider": "hubspot", + "revision": "1", + "adapterKey": "crm.associations.list", + "title": "List record associations", + "description": "List bounded associations from one reviewed CRM record to one reviewed object type.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["fromObjectType", "objectId", "toObjectType", "limit"], + "properties": { + "fromObjectType": { + "enum": ["companies", "contacts", "deals", "tickets"] + }, + "objectId": { "type": "string", "pattern": "^[1-9]\\d{0,19}$" }, + "toObjectType": { + "enum": ["companies", "contacts", "deals", "tickets"] + }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } + } + }, + "examples": [ + { + "input": { + "fromObjectType": "contacts", + "objectId": "123", + "toObjectType": "companies", + "limit": 100 + }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.associations.list --input @request.json --out hubspot-associations.json --dry-run" + } + ] + } + }, + { + "path": "/crm/objects/2026-03/{fromObjectType}/{objectId}/associations/default/{toObjectType}/{toObjectId}", + "method": "put", + "operationId": "crm.associations.create", + "exposure": "inventory", + "reason": "Mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/objects/2026-03/{fromObjectType}/{objectId}/associations/{toObjectType}/{toObjectId}", + "method": "delete", + "operationId": "crm.associations.delete", + "exposure": "inventory", + "reason": "Destructive mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/events/event-occurrences/2026-03", + "method": "get", + "operationId": "events.occurrences.list", + "exposure": "executable", + "capability": { + "id": "hubspot.events.occurrences.list", + "provider": "hubspot", + "revision": "1", + "adapterKey": "events.occurrences.list", + "title": "List event occurrences", + "description": "List bounded HubSpot event occurrences within an explicit time window.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "occurredAfter", + "occurredBefore", + "properties", + "limit" + ], + "properties": { + "occurredAfter": { + "type": "string", + "format": "date-time", + "maxLength": 64 + }, + "occurredBefore": { + "type": "string", + "format": "date-time", + "maxLength": 64 + }, + "eventType": { "type": "string", "minLength": 1, "maxLength": 200 }, + "objectType": { "enum": ["company", "contact", "deal", "ticket"] }, + "objectId": { "type": "string", "pattern": "^[1-9]\\d{0,19}$" }, + "properties": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "uniqueItems": true, + "items": { + "enum": [ + "hs_browser", + "hs_city", + "hs_content_type", + "hs_country", + "hs_device_name", + "hs_device_type", + "hs_page_title", + "hs_referrer", + "hs_touchpoint_source", + "hs_url", + "hs_utm_campaign", + "hs_utm_medium", + "hs_utm_source" + ] + } + }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } + } + }, + "examples": [ + { + "input": { + "occurredAfter": "2026-08-01T00:00:00Z", + "occurredBefore": "2026-08-02T00:00:00Z", + "eventType": "e_visited_page", + "properties": ["hs_url", "hs_page_title"], + "limit": 100 + }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.events.occurrences.list --input @request.json --out hubspot-events.json --dry-run" + } + ] + } + }, + { + "path": "/events/2026-03/send", + "method": "post", + "operationId": "events.occurrences.send", + "exposure": "inventory", + "reason": "Event sending is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/pipelines/2026-03/{objectType}", + "method": "get", + "operationId": "crm.pipelines.list", + "exposure": "executable", + "capability": { + "id": "hubspot.crm.pipelines.list", + "provider": "hubspot", + "revision": "1", + "adapterKey": "crm.pipelines.list", + "title": "List CRM pipelines", + "description": "List pipelines and stages for deals or tickets.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["objectType"], + "properties": { "objectType": { "enum": ["deals", "tickets"] } } + }, + "examples": [ + { + "input": { "objectType": "deals" }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.pipelines.list --input '{\"objectType\":\"deals\"}' --out hubspot-deal-pipelines.json --dry-run" + } + ] + } + }, + { + "path": "/crm/pipelines/2026-03/{objectType}", + "method": "post", + "operationId": "crm.pipelines.create", + "exposure": "inventory", + "reason": "Mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/pipelines/2026-03/{objectType}/{pipelineId}", + "method": "delete", + "operationId": "crm.pipelines.delete", + "exposure": "inventory", + "reason": "Destructive mutation is outside the read-only HubSpot V1 provider." + }, + { + "path": "/crm/owners/2026-03", + "method": "get", + "operationId": "crm.owners.list", + "exposure": "executable", + "capability": { + "id": "hubspot.crm.owners.list", + "provider": "hubspot", + "revision": "1", + "adapterKey": "crm.owners.list", + "title": "List CRM owners", + "description": "List bounded active or archived HubSpot owners for attribution joins.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["limit"], + "properties": { + "archived": { "type": "boolean" }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 100 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 5000 } + } + }, + "examples": [ + { + "input": { "limit": 100 }, + "command": "gkit --profile app-a hubspot api call --capability-id hubspot.crm.owners.list --input '{\"limit\":100}' --out hubspot-owners.json --dry-run" + } + ] + } + }, + { + "path": "/crm/imports/2026-03/imports", + "method": "post", + "operationId": "crm.imports.create", + "exposure": "inventory", + "reason": "Imports are mutations and are outside the read-only HubSpot V1 provider." + } + ] } diff --git a/packages/gkit/policy/pagespeed.reviewed.json b/packages/gkit/policy/pagespeed.reviewed.json index 77697a9..f345b35 100644 --- a/packages/gkit/policy/pagespeed.reviewed.json +++ b/packages/gkit/policy/pagespeed.reviewed.json @@ -1,63 +1,68 @@ { - "version": 1, - "provider": "pagespeed", - "manifestRevision": "2026-10-27.pagespeed.read-v1.1", - "reviewedAt": "2026-10-27T05:00:00.000Z", - "entries": [ - { - "path": "/pagespeedonline/v5/runPagespeed", - "method": "get", - "operationId": "pagespeedapi.runpagespeed", - "exposure": "executable", - "capability": { - "id": "pagespeed.run", - "provider": "pagespeed", - "revision": "1", - "adapterKey": "run", - "title": "Run a PageSpeed Insights analysis", - "description": "Run Lighthouse for one URL and return category scores plus Chrome UX Report field data for the URL and its origin when available.", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["url"], - "properties": { - "url": { - "type": "string", - "minLength": 1, - "maxLength": 2048 - }, - "strategy": { - "enum": ["mobile", "desktop"] - }, - "categories": { - "type": "array", - "minItems": 1, - "maxItems": 4, - "uniqueItems": true, - "items": { - "enum": ["performance", "seo", "accessibility", "best-practices"] - } - }, - "locale": { - "type": "string", - "minLength": 2, - "maxLength": 16 - } - } - }, - "examples": [ - { - "input": { - "url": "https://example.com/", - "strategy": "mobile", - "categories": ["performance", "seo"] - }, - "command": "gkit --profile pagespeed api call --capability-id pagespeed.run --input @request.json --out pagespeed.json --dry-run" - } - ] - } - } - ] + "version": 1, + "provider": "pagespeed", + "manifestRevision": "2026-10-27.pagespeed.read-v1.1", + "reviewedAt": "2026-10-27T05:00:00.000Z", + "entries": [ + { + "path": "/pagespeedonline/v5/runPagespeed", + "method": "get", + "operationId": "pagespeedapi.runpagespeed", + "exposure": "executable", + "capability": { + "id": "pagespeed.run", + "provider": "pagespeed", + "revision": "1", + "adapterKey": "run", + "title": "Run a PageSpeed Insights analysis", + "description": "Run Lighthouse for one URL and return category scores plus Chrome UX Report field data for the URL and its origin when available.", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["url"], + "properties": { + "url": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "strategy": { + "enum": ["mobile", "desktop"] + }, + "categories": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "uniqueItems": true, + "items": { + "enum": [ + "performance", + "seo", + "accessibility", + "best-practices" + ] + } + }, + "locale": { + "type": "string", + "minLength": 2, + "maxLength": 16 + } + } + }, + "examples": [ + { + "input": { + "url": "https://example.com/", + "strategy": "mobile", + "categories": ["performance", "seo"] + }, + "command": "gkit --profile pagespeed api call --capability-id pagespeed.run --input @request.json --out pagespeed.json --dry-run" + } + ] + } + } + ] } diff --git a/packages/gkit/policy/posthog.reviewed.json b/packages/gkit/policy/posthog.reviewed.json index 4172a6a..95b4126 100644 --- a/packages/gkit/policy/posthog.reviewed.json +++ b/packages/gkit/policy/posthog.reviewed.json @@ -1,82 +1,82 @@ { - "version": 1, - "provider": "posthog", - "manifestRevision": "2026-07-14.slice3.1", - "reviewedAt": "2026-07-14T03:04:00.000Z", - "entries": [ - { - "path": "/api/projects/{project_id}/query/", - "method": "post", - "operationId": "query_create", - "exposure": "executable", - "capability": { - "id": "posthog.query.run", - "provider": "posthog", - "revision": "1", - "title": "Run a bounded HogQL read", - "description": "Run one read-only HogQL SELECT or WITH query with an explicit result-row limit.", - "adapterKey": "query.run", - "effects": ["read"], - "inputSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, - "required": ["query", "limit"], - "properties": { - "query": { - "type": "string", - "minLength": 1, - "maxLength": 20000, - "pattern": "^\\s*(?:[Ss][Ee][Ll][Ee][Cc][Tt]|[Ww][Ii][Tt][Hh])\\b", - "allOf": [ - { - "not": { - "pattern": "[Ll][Ii][Mm][Ii][Tt]\\s+[0-9]+" - } - }, - { - "not": { - "pattern": ";" - } - }, - { - "not": { - "pattern": "(?:--|/\\*|\\*/|#)" - } - }, - { - "not": { - "pattern": "\\b(?:[Ii][Nn][Ss][Ee][Rr][Tt]|[Uu][Pp][Dd][Aa][Tt][Ee]|[Dd][Ee][Ll][Ee][Tt][Ee]|[Dd][Rr][Oo][Pp]|[Aa][Ll][Tt][Ee][Rr]|[Tt][Rr][Uu][Nn][Cc][Aa][Tt][Ee]|[Cc][Rr][Ee][Aa][Tt][Ee])\\b" - } - } - ] - }, - "limit": { - "type": "integer", - "minimum": 1, - "maximum": 1000 - } - } - }, - "examples": [ - { - "title": "Safe preflight", - "input": { - "query": "SELECT event, count() AS total FROM events GROUP BY event ORDER BY total DESC", - "limit": 20 - }, - "command": "gkit --profile clonesite.ai posthog api call --capability-id posthog.query.run --input @req.json --out posthog-query.json --dry-run" - }, - { - "title": "Live call after reviewing the dry-run", - "input": { - "query": "SELECT event, count() AS total FROM events GROUP BY event ORDER BY total DESC", - "limit": 20 - }, - "command": "gkit --profile clonesite.ai posthog api call --capability-id posthog.query.run --input @req.json --out posthog-query.json" - } - ] - } - } - ] + "version": 1, + "provider": "posthog", + "manifestRevision": "2026-07-14.slice3.1", + "reviewedAt": "2026-07-14T03:04:00.000Z", + "entries": [ + { + "path": "/api/projects/{project_id}/query/", + "method": "post", + "operationId": "query_create", + "exposure": "executable", + "capability": { + "id": "posthog.query.run", + "provider": "posthog", + "revision": "1", + "title": "Run a bounded HogQL read", + "description": "Run one read-only HogQL SELECT or WITH query with an explicit result-row limit.", + "adapterKey": "query.run", + "effects": ["read"], + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["query", "limit"], + "properties": { + "query": { + "type": "string", + "minLength": 1, + "maxLength": 20000, + "pattern": "^\\s*(?:[Ss][Ee][Ll][Ee][Cc][Tt]|[Ww][Ii][Tt][Hh])\\b", + "allOf": [ + { + "not": { + "pattern": "[Ll][Ii][Mm][Ii][Tt]\\s+[0-9]+" + } + }, + { + "not": { + "pattern": ";" + } + }, + { + "not": { + "pattern": "(?:--|/\\*|\\*/|#)" + } + }, + { + "not": { + "pattern": "\\b(?:[Ii][Nn][Ss][Ee][Rr][Tt]|[Uu][Pp][Dd][Aa][Tt][Ee]|[Dd][Ee][Ll][Ee][Tt][Ee]|[Dd][Rr][Oo][Pp]|[Aa][Ll][Tt][Ee][Rr]|[Tt][Rr][Uu][Nn][Cc][Aa][Tt][Ee]|[Cc][Rr][Ee][Aa][Tt][Ee])\\b" + } + } + ] + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000 + } + } + }, + "examples": [ + { + "title": "Safe preflight", + "input": { + "query": "SELECT event, count() AS total FROM events GROUP BY event ORDER BY total DESC", + "limit": 20 + }, + "command": "gkit --profile clonesite.ai posthog api call --capability-id posthog.query.run --input @req.json --out posthog-query.json --dry-run" + }, + { + "title": "Live call after reviewing the dry-run", + "input": { + "query": "SELECT event, count() AS total FROM events GROUP BY event ORDER BY total DESC", + "limit": 20 + }, + "command": "gkit --profile clonesite.ai posthog api call --capability-id posthog.query.run --input @req.json --out posthog-query.json" + } + ] + } + } + ] } diff --git a/packages/gkit/scripts/generate-bing.ts b/packages/gkit/scripts/generate-bing.ts index 8c136fe..4e63573 100644 --- a/packages/gkit/scripts/generate-bing.ts +++ b/packages/gkit/scripts/generate-bing.ts @@ -1,11 +1,11 @@ -import { resolve } from "node:path"; +import { resolve } from 'node:path' -import { writeContractProviderArtifacts } from "./generate-contract-provider"; +import { writeContractProviderArtifacts } from './generate-contract-provider' if (import.meta.main) { - await writeContractProviderArtifacts({ - packageRoot: resolve(new URL("..", import.meta.url).pathname), - provider: "bing", - check: process.argv.slice(2).includes("--check"), - }); + await writeContractProviderArtifacts({ + packageRoot: resolve(new URL('..', import.meta.url).pathname), + provider: 'bing', + check: process.argv.slice(2).includes('--check'), + }) } diff --git a/packages/gkit/scripts/generate-contract-provider.test.ts b/packages/gkit/scripts/generate-contract-provider.test.ts index c48f188..8c02ca9 100644 --- a/packages/gkit/scripts/generate-contract-provider.test.ts +++ b/packages/gkit/scripts/generate-contract-provider.test.ts @@ -1,26 +1,35 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest' -import { generateContractProviderArtifacts } from "./generate-contract-provider"; +import { generateContractProviderArtifacts } from './generate-contract-provider' -const packageRoot = new URL("..", import.meta.url).pathname; +const packageRoot = new URL('..', import.meta.url).pathname describe.each([ - ["bing", 21, "bing.sites.list"], - ["gsc", 6, "gsc.properties.list"], - ["pagespeed", 1, "pagespeed.run"], -] as const)("%s contract generator", (provider, expectedCount, expectedCapability) => { - it("is deterministic and binds reviewed capabilities to the pinned source", async () => { - const first = await generateContractProviderArtifacts({ packageRoot, provider }); - const second = await generateContractProviderArtifacts({ packageRoot, provider }); - expect(second).toEqual(first); + ['bing', 21, 'bing.sites.list'], + ['gsc', 6, 'gsc.properties.list'], + ['pagespeed', 1, 'pagespeed.run'], +] as const)( + '%s contract generator', + (provider, expectedCount, expectedCapability) => { + it('is deterministic and binds reviewed capabilities to the pinned source', async () => { + const first = await generateContractProviderArtifacts({ + packageRoot, + provider, + }) + const second = await generateContractProviderArtifacts({ + packageRoot, + provider, + }) + expect(second).toEqual(first) - const manifest = JSON.parse(first.manifest) as { - capabilities: Array<{ id: string; effects: string[] }>; - }; - expect(manifest.capabilities).toHaveLength(expectedCount); - expect(manifest.capabilities).toContainEqual( - expect.objectContaining({ id: expectedCapability, effects: ["read"] }), - ); - expect(first.docs).toContain(expectedCapability); - }); -}); + const manifest = JSON.parse(first.manifest) as { + capabilities: Array<{ id: string; effects: string[] }> + } + expect(manifest.capabilities).toHaveLength(expectedCount) + expect(manifest.capabilities).toContainEqual( + expect.objectContaining({ id: expectedCapability, effects: ['read'] }), + ) + expect(first.docs).toContain(expectedCapability) + }) + }, +) diff --git a/packages/gkit/scripts/generate-contract-provider.ts b/packages/gkit/scripts/generate-contract-provider.ts index aa1391d..ba8e971 100644 --- a/packages/gkit/scripts/generate-contract-provider.ts +++ b/packages/gkit/scripts/generate-contract-provider.ts @@ -1,279 +1,334 @@ -import { createHash } from "node:crypto"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, join } from "node:path"; +import { createHash } from 'node:crypto' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' -import { renderProviderDocs } from "../src/docs"; -import { compileExecutableManifest } from "../src/manifest"; +import { renderProviderDocs } from '../src/docs' +import { compileExecutableManifest } from '../src/manifest' -type JsonRecord = Record; +type JsonRecord = Record type SourceMetadata = { - url: string; - revision: string; - checksum: string; -}; + url: string + revision: string + checksum: string +} type SourceOperation = { - path: string; - method: string; - operationId: string; -}; + path: string + method: string + operationId: string +} type SourceContract = { - version: 1; - provider: string; - operations: SourceOperation[]; -}; + version: 1 + provider: string + operations: SourceOperation[] +} type ReviewedEntry = SourceOperation & { - exposure: "executable" | "inventory"; - reason?: string; - capability?: JsonRecord; -}; + exposure: 'executable' | 'inventory' + reason?: string + capability?: JsonRecord +} type ReviewedPolicy = { - version: 1; - provider: string; - manifestRevision: string; - reviewedAt: string; - entries: ReviewedEntry[]; -}; + version: 1 + provider: string + manifestRevision: string + reviewedAt: string + entries: ReviewedEntry[] +} type InventoryOperation = SourceOperation & { - exposure: "executable" | "inventory"; - capabilityId?: string; - reason: string; -}; + exposure: 'executable' | 'inventory' + capabilityId?: string + reason: string +} export type GeneratedContractProviderArtifacts = { - manifest: string; - inventory: string; - docs: string; - inventoryDocs: string; -}; + manifest: string + inventory: string + docs: string + inventoryDocs: string +} export async function generateContractProviderArtifacts(options: { - packageRoot: string; - provider: string; + packageRoot: string + provider: string }): Promise { - const sourceDirectory = join(options.packageRoot, "sources", options.provider); - const [sourceBytes, metadata, policy] = await Promise.all([ - readFile(join(sourceDirectory, "contract.json")), - readJson(join(sourceDirectory, "source.json")), - readJson( - join(options.packageRoot, "policy", `${options.provider}.reviewed.json`), - ), - ]); - const source = JSON.parse(sourceBytes.toString("utf8")) as SourceContract; - verifyInputs(options.provider, sourceBytes, source, metadata, policy); + const sourceDirectory = join(options.packageRoot, 'sources', options.provider) + const [sourceBytes, metadata, policy] = await Promise.all([ + readFile(join(sourceDirectory, 'contract.json')), + readJson(join(sourceDirectory, 'source.json')), + readJson( + join(options.packageRoot, 'policy', `${options.provider}.reviewed.json`), + ), + ]) + const source = JSON.parse(sourceBytes.toString('utf8')) as SourceContract + verifyInputs(options.provider, sourceBytes, source, metadata, policy) - const reviewed = new Map(policy.entries.map((entry) => [operationKey(entry), entry])); - if (reviewed.size !== policy.entries.length) { - throw new Error(`Reviewed ${options.provider} policy contains duplicate operations.`); - } - const inventoryOperations = [...source.operations] - .sort(compareOperations) - .map((operation): InventoryOperation => { - const entry = reviewed.get(operationKey(operation)); - if (!entry) { - return { - ...operation, - exposure: "inventory", - reason: "Not reviewed for executable gkit exposure.", - }; - } - if (entry.operationId !== operation.operationId) { - throw new Error(`Reviewed operation ${operationKey(operation)} has a stale operation ID.`); - } - if (entry.exposure === "inventory") { - if (!entry.reason) - throw new Error(`Inventory operation ${operationKey(entry)} needs a reason.`); - return { ...operation, exposure: "inventory", reason: entry.reason }; - } - if (!entry.capability) { - throw new Error(`Executable operation ${operationKey(entry)} has no capability.`); - } - return { - ...operation, - exposure: "executable", - capabilityId: String(entry.capability.id), - reason: "Reviewed adapter, input, effect, and response contracts are committed.", - }; - }); + const reviewed = new Map( + policy.entries.map((entry) => [operationKey(entry), entry]), + ) + if (reviewed.size !== policy.entries.length) { + throw new Error( + `Reviewed ${options.provider} policy contains duplicate operations.`, + ) + } + const inventoryOperations = [...source.operations] + .sort(compareOperations) + .map((operation): InventoryOperation => { + const entry = reviewed.get(operationKey(operation)) + if (!entry) { + return { + ...operation, + exposure: 'inventory', + reason: 'Not reviewed for executable gkit exposure.', + } + } + if (entry.operationId !== operation.operationId) { + throw new Error( + `Reviewed operation ${operationKey(operation)} has a stale operation ID.`, + ) + } + if (entry.exposure === 'inventory') { + if (!entry.reason) + throw new Error( + `Inventory operation ${operationKey(entry)} needs a reason.`, + ) + return { ...operation, exposure: 'inventory', reason: entry.reason } + } + if (!entry.capability) { + throw new Error( + `Executable operation ${operationKey(entry)} has no capability.`, + ) + } + return { + ...operation, + exposure: 'executable', + capabilityId: String(entry.capability.id), + reason: + 'Reviewed adapter, input, effect, and response contracts are committed.', + } + }) - for (const entry of policy.entries) { - if (!source.operations.some((operation) => operationKey(operation) === operationKey(entry))) { - throw new Error( - `Reviewed operation ${operationKey(entry)} is absent from the pinned source.`, - ); - } - } + for (const entry of policy.entries) { + if ( + !source.operations.some( + (operation) => operationKey(operation) === operationKey(entry), + ) + ) { + throw new Error( + `Reviewed operation ${operationKey(entry)} is absent from the pinned source.`, + ) + } + } - const sourceDescriptor = { - url: metadata.url, - revision: metadata.revision, - checksum: metadata.checksum, - }; - const capabilities = policy.entries - .filter((entry) => entry.exposure === "executable") - .map((entry) => entry.capability!) - .sort((left, right) => String(left.id).localeCompare(String(right.id))); - const manifest = stableJson({ - version: 1, - provider: options.provider, - revision: policy.manifestRevision, - source: sourceDescriptor, - reviewedAt: policy.reviewedAt, - capabilities, - }); - const compiled = compileExecutableManifest(JSON.parse(manifest)); - return { - manifest, - inventory: stableJson({ - version: 1, - provider: options.provider, - revision: policy.manifestRevision, - source: sourceDescriptor, - operations: inventoryOperations, - }), - docs: renderProviderDocs(compiled), - inventoryDocs: renderInventoryDocs( - options.provider, - policy.manifestRevision, - inventoryOperations, - ), - }; + const sourceDescriptor = { + url: metadata.url, + revision: metadata.revision, + checksum: metadata.checksum, + } + const capabilities = policy.entries + .filter((entry) => entry.exposure === 'executable') + .map((entry) => entry.capability!) + .sort((left, right) => String(left.id).localeCompare(String(right.id))) + const manifest = stableJson({ + version: 1, + provider: options.provider, + revision: policy.manifestRevision, + source: sourceDescriptor, + reviewedAt: policy.reviewedAt, + capabilities, + }) + const compiled = compileExecutableManifest(JSON.parse(manifest)) + return { + manifest, + inventory: stableJson({ + version: 1, + provider: options.provider, + revision: policy.manifestRevision, + source: sourceDescriptor, + operations: inventoryOperations, + }), + docs: renderProviderDocs(compiled), + inventoryDocs: renderInventoryDocs( + options.provider, + policy.manifestRevision, + inventoryOperations, + ), + } } export async function writeContractProviderArtifacts(options: { - packageRoot: string; - provider: string; - check: boolean; + packageRoot: string + provider: string + check: boolean }): Promise { - const artifacts = await generateContractProviderArtifacts(options); - const outputs = [ - [join(options.packageRoot, "generated", options.provider, "manifest.json"), artifacts.manifest], - [ - join(options.packageRoot, "generated", options.provider, "inventory.json"), - artifacts.inventory, - ], - [ - join(options.packageRoot, "docs/providers", options.provider, "capabilities.md"), - artifacts.docs, - ], - [ - join(options.packageRoot, "docs/providers", options.provider, "inventory.md"), - artifacts.inventoryDocs, - ], - ] as const; + const artifacts = await generateContractProviderArtifacts(options) + const outputs = [ + [ + join(options.packageRoot, 'generated', options.provider, 'manifest.json'), + artifacts.manifest, + ], + [ + join( + options.packageRoot, + 'generated', + options.provider, + 'inventory.json', + ), + artifacts.inventory, + ], + [ + join( + options.packageRoot, + 'docs/providers', + options.provider, + 'capabilities.md', + ), + artifacts.docs, + ], + [ + join( + options.packageRoot, + 'docs/providers', + options.provider, + 'inventory.md', + ), + artifacts.inventoryDocs, + ], + ] as const - if (options.check) { - const drifted: string[] = []; - for (const [path, expected] of outputs) { - try { - if ((await readFile(path, "utf8")) !== expected) drifted.push(path); - } catch { - drifted.push(path); - } - } - if (drifted.length > 0) { - throw new Error(`Generated ${options.provider} artifacts are stale: ${drifted.join(", ")}`); - } - return; - } + if (options.check) { + const drifted: string[] = [] + for (const [path, expected] of outputs) { + try { + if ((await readFile(path, 'utf8')) !== expected) drifted.push(path) + } catch { + drifted.push(path) + } + } + if (drifted.length > 0) { + throw new Error( + `Generated ${options.provider} artifacts are stale: ${drifted.join(', ')}`, + ) + } + return + } - for (const [path, contents] of outputs) { - await mkdir(dirname(path), { recursive: true }); - await writeFile(path, contents, "utf8"); - } + for (const [path, contents] of outputs) { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, contents, 'utf8') + } } function verifyInputs( - provider: string, - sourceBytes: Uint8Array, - source: SourceContract, - metadata: SourceMetadata, - policy: ReviewedPolicy, + provider: string, + sourceBytes: Uint8Array, + source: SourceContract, + metadata: SourceMetadata, + policy: ReviewedPolicy, ): void { - const checksum = `sha256:${createHash("sha256").update(sourceBytes).digest("hex")}`; - if (checksum !== metadata.checksum) { - throw new Error(`Pinned ${provider} source checksum mismatch: expected ${metadata.checksum}.`); - } - if (source.version !== 1 || source.provider !== provider || !Array.isArray(source.operations)) { - throw new Error(`Pinned ${provider} source contract is structurally invalid.`); - } - if ( - policy.version !== 1 || - policy.provider !== provider || - !policy.manifestRevision || - !policy.reviewedAt || - !Array.isArray(policy.entries) - ) { - throw new Error(`Reviewed ${provider} policy is structurally invalid.`); - } + const checksum = `sha256:${createHash('sha256').update(sourceBytes).digest('hex')}` + if (checksum !== metadata.checksum) { + throw new Error( + `Pinned ${provider} source checksum mismatch: expected ${metadata.checksum}.`, + ) + } + if ( + source.version !== 1 || + source.provider !== provider || + !Array.isArray(source.operations) + ) { + throw new Error( + `Pinned ${provider} source contract is structurally invalid.`, + ) + } + if ( + policy.version !== 1 || + policy.provider !== provider || + !policy.manifestRevision || + !policy.reviewedAt || + !Array.isArray(policy.entries) + ) { + throw new Error(`Reviewed ${provider} policy is structurally invalid.`) + } } function renderInventoryDocs( - provider: string, - revision: string, - operations: InventoryOperation[], + provider: string, + revision: string, + operations: InventoryOperation[], ): string { - const executable = operations.filter((operation) => operation.exposure === "executable").length; - const title = - provider === "gsc" - ? "Google Search Console" - : provider === "hubspot" - ? "HubSpot" - : provider === "pagespeed" - ? "PageSpeed Insights" - : "Bing Webmaster"; - const lines = [ - "---", - "type: Reference", - `title: ${title} operation inventory`, - "description: >", - ` Generated inventory of pinned ${title} operations and their gkit exposure decisions.`, - `provider: ${provider}`, - `inventoryRevision: ${revision}`, - "---", - "", - `# ${title} operation inventory`, - "", - `This pinned inventory contains ${operations.length} operations: ${executable} executable and ${operations.length - executable} inventory-only.`, - `Inventory-only operations cannot be routed by \`gkit ${provider} api call\`.`, - "", - "| Method | Path | Operation ID | Exposure | Decision |", - "| --- | --- | --- | --- | --- |", - ]; - if (provider === "hubspot") { - lines.splice( - lines.length - 2, - 0, - "HubSpot record, owner, event, association, pipeline, and property artifacts may contain PII or confidential business data. The inventory records endpoint exposure only; it does not authorize copying provider data into logs or prompts.", - "", - ); - } - for (const operation of operations) { - const capability = operation.capabilityId ? `; capability: \`${operation.capabilityId}\`` : ""; - lines.push( - `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capability} |`, - ); - } - return `${lines.join("\n")}\n`; + const executable = operations.filter( + (operation) => operation.exposure === 'executable', + ).length + const title = + provider === 'gsc' + ? 'Google Search Console' + : provider === 'hubspot' + ? 'HubSpot' + : provider === 'pagespeed' + ? 'PageSpeed Insights' + : 'Bing Webmaster' + const lines = [ + '---', + 'type: Reference', + `title: ${title} operation inventory`, + 'description: >', + ` Generated inventory of pinned ${title} operations and their gkit exposure decisions.`, + `provider: ${provider}`, + `inventoryRevision: ${revision}`, + '---', + '', + `# ${title} operation inventory`, + '', + `This pinned inventory contains ${operations.length} operations: ${executable} executable and ${operations.length - executable} inventory-only.`, + `Inventory-only operations cannot be routed by \`gkit ${provider} api call\`.`, + '', + '| Method | Path | Operation ID | Exposure | Decision |', + '| --- | --- | --- | --- | --- |', + ] + if (provider === 'hubspot') { + lines.splice( + lines.length - 2, + 0, + 'HubSpot record, owner, event, association, pipeline, and property artifacts may contain PII or confidential business data. The inventory records endpoint exposure only; it does not authorize copying provider data into logs or prompts.', + '', + ) + } + for (const operation of operations) { + const capability = operation.capabilityId + ? `; capability: \`${operation.capabilityId}\`` + : '' + lines.push( + `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capability} |`, + ) + } + return `${lines.join('\n')}\n` } -function operationKey(operation: Pick): string { - return `${operation.method.toLowerCase()} ${operation.path}`; +function operationKey( + operation: Pick, +): string { + return `${operation.method.toLowerCase()} ${operation.path}` } -function compareOperations(left: SourceOperation, right: SourceOperation): number { - return left.path.localeCompare(right.path) || left.method.localeCompare(right.method); +function compareOperations( + left: SourceOperation, + right: SourceOperation, +): number { + return ( + left.path.localeCompare(right.path) || + left.method.localeCompare(right.method) + ) } function stableJson(value: unknown): string { - return `${JSON.stringify(value, null, 2)}\n`; + return `${JSON.stringify(value, null, 2)}\n` } async function readJson(path: string): Promise { - return JSON.parse(await readFile(path, "utf8")) as T; + return JSON.parse(await readFile(path, 'utf8')) as T } diff --git a/packages/gkit/scripts/generate-dataforseo.test.ts b/packages/gkit/scripts/generate-dataforseo.test.ts index a5dcc81..5084e47 100644 --- a/packages/gkit/scripts/generate-dataforseo.test.ts +++ b/packages/gkit/scripts/generate-dataforseo.test.ts @@ -1,44 +1,40 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest' -import { generateDataForSeoArtifacts } from "./generate-dataforseo"; +import { generateDataForSeoArtifacts } from './generate-dataforseo' -const packageRoot = new URL("..", import.meta.url).pathname; +const packageRoot = new URL('..', import.meta.url).pathname -describe("DataForSEO artifact generator", () => { - it( - "projects one pinned source and reviewed policy into executable and inventory surfaces", - async () => { - const first = await generateDataForSeoArtifacts(packageRoot); - const second = await generateDataForSeoArtifacts(packageRoot); +describe('DataForSEO artifact generator', () => { + it('projects one pinned source and reviewed policy into executable and inventory surfaces', async () => { + const first = await generateDataForSeoArtifacts(packageRoot) + const second = await generateDataForSeoArtifacts(packageRoot) - expect(second).toEqual(first); + expect(second).toEqual(first) - const manifest = JSON.parse(first.manifest) as { - capabilities: Array<{ id: string }>; - }; - const inventory = JSON.parse(first.inventory) as { - operations: Array<{ - operationId: string; - exposure: "executable" | "inventory"; - reason: string; - }>; - }; + const manifest = JSON.parse(first.manifest) as { + capabilities: Array<{ id: string }> + } + const inventory = JSON.parse(first.inventory) as { + operations: Array<{ + operationId: string + exposure: 'executable' | 'inventory' + reason: string + }> + } - expect(manifest.capabilities.map((record) => record.id)).toEqual([ - "dataforseo.ai_optimization.llm_mentions.search.live", - "dataforseo.backlinks.bulk_ranks.live", - "dataforseo.backlinks.referring_domains.live", - "dataforseo.backlinks.summary.live", - "dataforseo.serp.google.organic.live.advanced", - ]); - expect(inventory.operations).toContainEqual( - expect.objectContaining({ - operationId: "LlmMentionsSearchLive", - exposure: "executable", - capabilityId: "dataforseo.ai_optimization.llm_mentions.search.live", - }), - ); - }, - 15_000, - ); -}); + expect(manifest.capabilities.map((record) => record.id)).toEqual([ + 'dataforseo.ai_optimization.llm_mentions.search.live', + 'dataforseo.backlinks.bulk_ranks.live', + 'dataforseo.backlinks.referring_domains.live', + 'dataforseo.backlinks.summary.live', + 'dataforseo.serp.google.organic.live.advanced', + ]) + expect(inventory.operations).toContainEqual( + expect.objectContaining({ + operationId: 'LlmMentionsSearchLive', + exposure: 'executable', + capabilityId: 'dataforseo.ai_optimization.llm_mentions.search.live', + }), + ) + }, 15_000) +}) diff --git a/packages/gkit/scripts/generate-dataforseo.ts b/packages/gkit/scripts/generate-dataforseo.ts index e0c1402..cdb4c89 100644 --- a/packages/gkit/scripts/generate-dataforseo.ts +++ b/packages/gkit/scripts/generate-dataforseo.ts @@ -1,295 +1,344 @@ -import { createHash } from "node:crypto"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; +import { createHash } from 'node:crypto' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join, resolve } from 'node:path' +import { parse } from 'yaml' -import { parse } from "yaml"; +import { renderProviderDocs } from '../src/docs' +import { compileExecutableManifest } from '../src/manifest' -import { renderProviderDocs } from "../src/docs"; -import { compileExecutableManifest } from "../src/manifest"; - -type JsonRecord = Record; +type JsonRecord = Record type SourceMetadata = { - repository: string; - path: string; - revision: string; - checksum: string; - retrievedAt: string; -}; + repository: string + path: string + revision: string + checksum: string + retrievedAt: string +} type ReviewedEntry = { - path: string; - method: string; - operationId: string; - exposure: "executable" | "inventory"; - reason?: string; - capability?: JsonRecord; -}; + path: string + method: string + operationId: string + exposure: 'executable' | 'inventory' + reason?: string + capability?: JsonRecord +} type ReviewedPolicy = { - version: 1; - provider: string; - manifestRevision: string; - reviewedAt: string; - entries: ReviewedEntry[]; -}; + version: 1 + provider: string + manifestRevision: string + reviewedAt: string + entries: ReviewedEntry[] +} type InventoryOperation = { - path: string; - method: string; - operationId: string; - tags: string[]; - exposure: "executable" | "inventory"; - capabilityId?: string; - reason: string; -}; + path: string + method: string + operationId: string + tags: string[] + exposure: 'executable' | 'inventory' + capabilityId?: string + reason: string +} export type GeneratedDataForSeoArtifacts = { - manifest: string; - inventory: string; - docs: string; - inventoryDocs: string; -}; + manifest: string + inventory: string + docs: string + inventoryDocs: string +} -const httpMethods = new Set(["delete", "get", "head", "options", "patch", "post", "put"]); +const httpMethods = new Set([ + 'delete', + 'get', + 'head', + 'options', + 'patch', + 'post', + 'put', +]) export async function generateDataForSeoArtifacts( - packageRoot: string, + packageRoot: string, ): Promise { - const sourceDirectory = join(packageRoot, "sources/dataforseo"); - const sourcePath = join(sourceDirectory, "openapi.yaml"); - const [sourceBytes, sourceMetadata, policy] = await Promise.all([ - readFile(sourcePath), - readJson(join(sourceDirectory, "source.json")), - readJson(join(packageRoot, "policy/dataforseo.reviewed.json")), - ]); + const sourceDirectory = join(packageRoot, 'sources/dataforseo') + const sourcePath = join(sourceDirectory, 'openapi.yaml') + const [sourceBytes, sourceMetadata, policy] = await Promise.all([ + readFile(sourcePath), + readJson(join(sourceDirectory, 'source.json')), + readJson( + join(packageRoot, 'policy/dataforseo.reviewed.json'), + ), + ]) - verifySource(sourceBytes, sourceMetadata); - verifyPolicy(policy); + verifySource(sourceBytes, sourceMetadata) + verifyPolicy(policy) - const openApi = asRecord(parse(sourceBytes.toString("utf8"))); - const operations = collectOperations(openApi); - const operationsByKey = new Map( - operations.map((operation) => [operationKey(operation.path, operation.method), operation]), - ); + const openApi = asRecord(parse(sourceBytes.toString('utf8'))) + const operations = collectOperations(openApi) + const operationsByKey = new Map( + operations.map((operation) => [ + operationKey(operation.path, operation.method), + operation, + ]), + ) - const reviewedByKey = new Map(); - for (const entry of policy.entries) { - const key = operationKey(entry.path, entry.method); - if (reviewedByKey.has(key)) { - throw new Error(`Reviewed DataForSEO policy contains duplicate operation ${key}.`); - } - const operation = operationsByKey.get(key); - if (!operation) { - throw new Error(`Reviewed DataForSEO operation ${key} is absent from the pinned source.`); - } - if (operation.operationId !== entry.operationId) { - throw new Error( - `Reviewed operation ${key} expected ${entry.operationId}, found ${operation.operationId}.`, - ); - } - if (entry.exposure === "executable" && !entry.capability) { - throw new Error(`Executable DataForSEO operation ${key} has no capability record.`); - } - if (entry.exposure === "inventory" && !entry.reason) { - throw new Error(`Inventory-only DataForSEO operation ${key} requires a reason.`); - } - reviewedByKey.set(key, entry); - } + const reviewedByKey = new Map() + for (const entry of policy.entries) { + const key = operationKey(entry.path, entry.method) + if (reviewedByKey.has(key)) { + throw new Error( + `Reviewed DataForSEO policy contains duplicate operation ${key}.`, + ) + } + const operation = operationsByKey.get(key) + if (!operation) { + throw new Error( + `Reviewed DataForSEO operation ${key} is absent from the pinned source.`, + ) + } + if (operation.operationId !== entry.operationId) { + throw new Error( + `Reviewed operation ${key} expected ${entry.operationId}, found ${operation.operationId}.`, + ) + } + if (entry.exposure === 'executable' && !entry.capability) { + throw new Error( + `Executable DataForSEO operation ${key} has no capability record.`, + ) + } + if (entry.exposure === 'inventory' && !entry.reason) { + throw new Error( + `Inventory-only DataForSEO operation ${key} requires a reason.`, + ) + } + reviewedByKey.set(key, entry) + } - const capabilities = policy.entries - .filter((entry) => entry.exposure === "executable") - .map((entry) => entry.capability!) - .sort((left, right) => String(left.id).localeCompare(String(right.id))); - const source = { - url: `${sourceMetadata.repository}/blob/${sourceMetadata.revision}/${sourceMetadata.path}`, - revision: sourceMetadata.revision, - checksum: sourceMetadata.checksum, - }; - const manifestDocument = { - version: 1, - provider: policy.provider, - revision: policy.manifestRevision, - source, - reviewedAt: policy.reviewedAt, - capabilities, - }; - const manifest = stableJson(manifestDocument); - const compiledManifest = compileExecutableManifest(JSON.parse(manifest)); + const capabilities = policy.entries + .filter((entry) => entry.exposure === 'executable') + .map((entry) => entry.capability!) + .sort((left, right) => String(left.id).localeCompare(String(right.id))) + const source = { + url: `${sourceMetadata.repository}/blob/${sourceMetadata.revision}/${sourceMetadata.path}`, + revision: sourceMetadata.revision, + checksum: sourceMetadata.checksum, + } + const manifestDocument = { + version: 1, + provider: policy.provider, + revision: policy.manifestRevision, + source, + reviewedAt: policy.reviewedAt, + capabilities, + } + const manifest = stableJson(manifestDocument) + const compiledManifest = compileExecutableManifest(JSON.parse(manifest)) - const inventoryOperations = operations.map((operation): InventoryOperation => { - const reviewed = reviewedByKey.get(operationKey(operation.path, operation.method)); - if (!reviewed) { - return { - ...operation, - exposure: "inventory", - reason: "Not reviewed for executable gkit exposure.", - }; - } - if (reviewed.exposure === "inventory") { - return { - ...operation, - exposure: "inventory", - reason: reviewed.reason!, - }; - } - return { - ...operation, - exposure: "executable", - capabilityId: String(reviewed.capability!.id), - reason: "Reviewed adapter, input, effect, and cost contracts are committed.", - }; - }); + const inventoryOperations = operations.map( + (operation): InventoryOperation => { + const reviewed = reviewedByKey.get( + operationKey(operation.path, operation.method), + ) + if (!reviewed) { + return { + ...operation, + exposure: 'inventory', + reason: 'Not reviewed for executable gkit exposure.', + } + } + if (reviewed.exposure === 'inventory') { + return { + ...operation, + exposure: 'inventory', + reason: reviewed.reason!, + } + } + return { + ...operation, + exposure: 'executable', + capabilityId: String(reviewed.capability!.id), + reason: + 'Reviewed adapter, input, effect, and cost contracts are committed.', + } + }, + ) - return { - manifest, - inventory: stableJson({ - version: 1, - provider: policy.provider, - revision: policy.manifestRevision, - source, - operations: inventoryOperations, - }), - docs: renderProviderDocs(compiledManifest), - inventoryDocs: renderInventoryDocs(policy, inventoryOperations), - }; + return { + manifest, + inventory: stableJson({ + version: 1, + provider: policy.provider, + revision: policy.manifestRevision, + source, + operations: inventoryOperations, + }), + docs: renderProviderDocs(compiledManifest), + inventoryDocs: renderInventoryDocs(policy, inventoryOperations), + } } async function run(): Promise { - const packageRoot = resolve(new URL("..", import.meta.url).pathname); - const check = process.argv.slice(2).includes("--check"); - const artifacts = await generateDataForSeoArtifacts(packageRoot); - const outputs = [ - [join(packageRoot, "generated/dataforseo/manifest.json"), artifacts.manifest], - [join(packageRoot, "generated/dataforseo/inventory.json"), artifacts.inventory], - [join(packageRoot, "docs/providers/dataforseo/backlinks.md"), artifacts.docs], - [join(packageRoot, "docs/providers/dataforseo/inventory.md"), artifacts.inventoryDocs], - ] as const; + const packageRoot = resolve(new URL('..', import.meta.url).pathname) + const check = process.argv.slice(2).includes('--check') + const artifacts = await generateDataForSeoArtifacts(packageRoot) + const outputs = [ + [ + join(packageRoot, 'generated/dataforseo/manifest.json'), + artifacts.manifest, + ], + [ + join(packageRoot, 'generated/dataforseo/inventory.json'), + artifacts.inventory, + ], + [ + join(packageRoot, 'docs/providers/dataforseo/backlinks.md'), + artifacts.docs, + ], + [ + join(packageRoot, 'docs/providers/dataforseo/inventory.md'), + artifacts.inventoryDocs, + ], + ] as const - if (check) { - const drifted: string[] = []; - for (const [path, expected] of outputs) { - try { - if ((await readFile(path, "utf8")) !== expected) drifted.push(path); - } catch { - drifted.push(path); - } - } - if (drifted.length > 0) { - throw new Error(`Generated DataForSEO artifacts are stale: ${drifted.join(", ")}`); - } - return; - } + if (check) { + const drifted: string[] = [] + for (const [path, expected] of outputs) { + try { + if ((await readFile(path, 'utf8')) !== expected) drifted.push(path) + } catch { + drifted.push(path) + } + } + if (drifted.length > 0) { + throw new Error( + `Generated DataForSEO artifacts are stale: ${drifted.join(', ')}`, + ) + } + return + } - for (const [path, contents] of outputs) { - await mkdir(dirname(path), { recursive: true }); - await writeFile(path, contents, "utf8"); - } + for (const [path, contents] of outputs) { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, contents, 'utf8') + } } -function renderInventoryDocs(policy: ReviewedPolicy, operations: InventoryOperation[]): string { - const executableCount = operations.filter( - (operation) => operation.exposure === "executable", - ).length; - const lines = [ - "---", - "type: Reference", - "title: DataForSEO operation inventory", - "description: >", - " Generated inventory of pinned DataForSEO operations and their gkit exposure decisions.", - `provider: ${policy.provider}`, - `inventoryRevision: ${policy.manifestRevision}`, - "---", - "", - "# DataForSEO operation inventory", - "", - `This pinned inventory contains ${operations.length} operations: ${executableCount} executable and ${operations.length - executableCount} inventory-only.`, - "Inventory-only operations cannot be routed by `gkit dataforseo api call`.", - "", - "| Method | Path | Operation ID | Exposure | Decision |", - "| --- | --- | --- | --- | --- |", - ]; - for (const operation of operations) { - const capability = operation.capabilityId ? `; capability: \`${operation.capabilityId}\`` : ""; - lines.push( - `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capability} |`, - ); - } - return `${lines.join("\n")}\n`; +function renderInventoryDocs( + policy: ReviewedPolicy, + operations: InventoryOperation[], +): string { + const executableCount = operations.filter( + (operation) => operation.exposure === 'executable', + ).length + const lines = [ + '---', + 'type: Reference', + 'title: DataForSEO operation inventory', + 'description: >', + ' Generated inventory of pinned DataForSEO operations and their gkit exposure decisions.', + `provider: ${policy.provider}`, + `inventoryRevision: ${policy.manifestRevision}`, + '---', + '', + '# DataForSEO operation inventory', + '', + `This pinned inventory contains ${operations.length} operations: ${executableCount} executable and ${operations.length - executableCount} inventory-only.`, + 'Inventory-only operations cannot be routed by `gkit dataforseo api call`.', + '', + '| Method | Path | Operation ID | Exposure | Decision |', + '| --- | --- | --- | --- | --- |', + ] + for (const operation of operations) { + const capability = operation.capabilityId + ? `; capability: \`${operation.capabilityId}\`` + : '' + lines.push( + `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capability} |`, + ) + } + return `${lines.join('\n')}\n` } function collectOperations(openApi: JsonRecord): InventoryOperation[] { - const paths = asRecord(openApi.paths); - return Object.entries(paths) - .flatMap(([path, pathValue]) => { - const pathRecord = asRecord(pathValue); - return Object.entries(pathRecord) - .filter(([method]) => httpMethods.has(method.toLowerCase())) - .map(([method, operationValue]) => { - const operation = asRecord(operationValue); - const operationId = operation.operationId; - if (typeof operationId !== "string" || operationId.length === 0) { - throw new Error( - `Pinned DataForSEO operation ${method.toUpperCase()} ${path} has no operationId.`, - ); - } - const tags = Array.isArray(operation.tags) - ? operation.tags.filter((tag): tag is string => typeof tag === "string").sort() - : []; - return { - path, - method: method.toLowerCase(), - operationId, - tags, - exposure: "inventory" as const, - reason: "", - }; - }); - }) - .sort( - (left, right) => - left.path.localeCompare(right.path) || left.method.localeCompare(right.method), - ); + const paths = asRecord(openApi.paths) + return Object.entries(paths) + .flatMap(([path, pathValue]) => { + const pathRecord = asRecord(pathValue) + return Object.entries(pathRecord) + .filter(([method]) => httpMethods.has(method.toLowerCase())) + .map(([method, operationValue]) => { + const operation = asRecord(operationValue) + const operationId = operation.operationId + if (typeof operationId !== 'string' || operationId.length === 0) { + throw new Error( + `Pinned DataForSEO operation ${method.toUpperCase()} ${path} has no operationId.`, + ) + } + const tags = Array.isArray(operation.tags) + ? operation.tags + .filter((tag): tag is string => typeof tag === 'string') + .sort() + : [] + return { + path, + method: method.toLowerCase(), + operationId, + tags, + exposure: 'inventory' as const, + reason: '', + } + }) + }) + .sort( + (left, right) => + left.path.localeCompare(right.path) || + left.method.localeCompare(right.method), + ) } function verifySource(bytes: Uint8Array, metadata: SourceMetadata): void { - const checksum = `sha256:${createHash("sha256").update(bytes).digest("hex")}`; - if (checksum !== metadata.checksum) { - throw new Error(`Pinned DataForSEO source checksum mismatch: expected ${metadata.checksum}.`); - } + const checksum = `sha256:${createHash('sha256').update(bytes).digest('hex')}` + if (checksum !== metadata.checksum) { + throw new Error( + `Pinned DataForSEO source checksum mismatch: expected ${metadata.checksum}.`, + ) + } } function verifyPolicy(policy: ReviewedPolicy): void { - if ( - policy.version !== 1 || - policy.provider !== "dataforseo" || - !policy.manifestRevision || - !policy.reviewedAt || - !Array.isArray(policy.entries) - ) { - throw new Error("Reviewed DataForSEO policy is structurally invalid."); - } + if ( + policy.version !== 1 || + policy.provider !== 'dataforseo' || + !policy.manifestRevision || + !policy.reviewedAt || + !Array.isArray(policy.entries) + ) { + throw new Error('Reviewed DataForSEO policy is structurally invalid.') + } } function operationKey(path: string, method: string): string { - return `${method.toLowerCase()} ${path}`; + return `${method.toLowerCase()} ${path}` } function stableJson(value: unknown): string { - return `${JSON.stringify(value, null, 2)}\n`; + return `${JSON.stringify(value, null, 2)}\n` } function asRecord(value: unknown): JsonRecord { - if (value === null || typeof value !== "object" || Array.isArray(value)) { - throw new Error("Pinned DataForSEO source is not an OpenAPI object."); - } - return value as JsonRecord; + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error('Pinned DataForSEO source is not an OpenAPI object.') + } + return value as JsonRecord } async function readJson(path: string): Promise { - return JSON.parse(await readFile(path, "utf8")) as T; + return JSON.parse(await readFile(path, 'utf8')) as T } if (import.meta.main) { - await run(); + await run() } diff --git a/packages/gkit/scripts/generate-google-ads.test.ts b/packages/gkit/scripts/generate-google-ads.test.ts index b167836..41fd748 100644 --- a/packages/gkit/scripts/generate-google-ads.test.ts +++ b/packages/gkit/scripts/generate-google-ads.test.ts @@ -1,43 +1,53 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest' -import { generateGoogleAdsArtifacts } from "./generate-google-ads"; +import { generateGoogleAdsArtifacts } from './generate-google-ads' -const packageRoot = new URL("..", import.meta.url).pathname; +const packageRoot = new URL('..', import.meta.url).pathname -describe("Google Ads artifact generator", () => { - it("projects five pinned REST methods into six reviewed read capabilities", async () => { - const first = await generateGoogleAdsArtifacts(packageRoot); - const second = await generateGoogleAdsArtifacts(packageRoot); +describe('Google Ads artifact generator', () => { + it('projects five pinned REST methods into six reviewed read capabilities', async () => { + const first = await generateGoogleAdsArtifacts(packageRoot) + const second = await generateGoogleAdsArtifacts(packageRoot) - expect(second).toEqual(first); + expect(second).toEqual(first) - const manifest = JSON.parse(first.manifest) as { - source: { revision: string; checksum: string }; - capabilities: Array<{ id: string; adapterKey: string; effects: string[] }>; - }; - const inventory = JSON.parse(first.inventory) as { - operations: Array<{ operationId: string; exposure: "executable" | "inventory" }>; - }; + const manifest = JSON.parse(first.manifest) as { + source: { revision: string; checksum: string } + capabilities: Array<{ id: string; adapterKey: string; effects: string[] }> + } + const inventory = JSON.parse(first.inventory) as { + operations: Array<{ + operationId: string + exposure: 'executable' | 'inventory' + }> + } - expect(manifest.source).toMatchObject({ - revision: "20260624", - checksum: "sha256:202028d3abcb9e4681d35f3c28d06e6ced1eaac2ec57c56357c8ab5d522841d7", - }); - expect(manifest.capabilities.map((capability) => capability.id)).toEqual([ - "google-ads.customers.list-accessible", - "google-ads.fields.describe", - "google-ads.fields.search", - "google-ads.keyword-plan.generate-historical-metrics", - "google-ads.keyword-plan.generate-ideas", - "google-ads.query.gaql", - ]); - expect(manifest.capabilities.every((capability) => capability.effects.join() === "read")).toBe( - true, - ); - expect( - inventory.operations.filter((operation) => operation.exposure === "executable"), - ).toHaveLength(5); - expect(first.docs).toContain("google-ads.query.gaql"); - expect(first.inventoryDocs).toContain("googleads.customers.googleAds.search"); - }, 20_000); -}); + expect(manifest.source).toMatchObject({ + revision: '20260624', + checksum: + 'sha256:202028d3abcb9e4681d35f3c28d06e6ced1eaac2ec57c56357c8ab5d522841d7', + }) + expect(manifest.capabilities.map((capability) => capability.id)).toEqual([ + 'google-ads.customers.list-accessible', + 'google-ads.fields.describe', + 'google-ads.fields.search', + 'google-ads.keyword-plan.generate-historical-metrics', + 'google-ads.keyword-plan.generate-ideas', + 'google-ads.query.gaql', + ]) + expect( + manifest.capabilities.every( + (capability) => capability.effects.join() === 'read', + ), + ).toBe(true) + expect( + inventory.operations.filter( + (operation) => operation.exposure === 'executable', + ), + ).toHaveLength(5) + expect(first.docs).toContain('google-ads.query.gaql') + expect(first.inventoryDocs).toContain( + 'googleads.customers.googleAds.search', + ) + }, 20_000) +}) diff --git a/packages/gkit/scripts/generate-google-ads.ts b/packages/gkit/scripts/generate-google-ads.ts index 0d4895a..35003d2 100644 --- a/packages/gkit/scripts/generate-google-ads.ts +++ b/packages/gkit/scripts/generate-google-ads.ts @@ -1,293 +1,343 @@ -import { createHash } from "node:crypto"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; +import { createHash } from 'node:crypto' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join, resolve } from 'node:path' -import { renderProviderDocs } from "../src/docs"; -import { compileExecutableManifest } from "../src/manifest"; +import { renderProviderDocs } from '../src/docs' +import { compileExecutableManifest } from '../src/manifest' -type JsonRecord = Record; +type JsonRecord = Record type SourceMetadata = { - url: string; - revision: string; - checksum: string; - retrievedAt: string; -}; + url: string + revision: string + checksum: string + retrievedAt: string +} type ReviewedEntry = { - path: string; - method: string; - operationId: string; - exposure: "executable" | "inventory"; - reason?: string; - capabilities?: JsonRecord[]; -}; + path: string + method: string + operationId: string + exposure: 'executable' | 'inventory' + reason?: string + capabilities?: JsonRecord[] +} type ReviewedPolicy = { - version: 1; - provider: "google-ads"; - manifestRevision: string; - reviewedAt: string; - sourceChecksum: string; - entries: ReviewedEntry[]; -}; + version: 1 + provider: 'google-ads' + manifestRevision: string + reviewedAt: string + sourceChecksum: string + entries: ReviewedEntry[] +} type InventoryOperation = { - path: string; - method: string; - operationId: string; - exposure: "executable" | "inventory"; - capabilityIds?: string[]; - reason: string; -}; + path: string + method: string + operationId: string + exposure: 'executable' | 'inventory' + capabilityIds?: string[] + reason: string +} export type GeneratedGoogleAdsArtifacts = { - manifest: string; - inventory: string; - docs: string; - inventoryDocs: string; -}; + manifest: string + inventory: string + docs: string + inventoryDocs: string +} export async function generateGoogleAdsArtifacts( - packageRoot: string, + packageRoot: string, ): Promise { - const sourceDirectory = join(packageRoot, "sources/google-ads"); - const [sourceBytes, sourceMetadata, policy] = await Promise.all([ - readFile(join(sourceDirectory, "discovery-v24.json")), - readJson(join(sourceDirectory, "source.json")), - readJson(join(packageRoot, "policy/google-ads.reviewed.json")), - ]); + const sourceDirectory = join(packageRoot, 'sources/google-ads') + const [sourceBytes, sourceMetadata, policy] = await Promise.all([ + readFile(join(sourceDirectory, 'discovery-v24.json')), + readJson(join(sourceDirectory, 'source.json')), + readJson( + join(packageRoot, 'policy/google-ads.reviewed.json'), + ), + ]) - verifySource(sourceBytes, sourceMetadata); - verifyPolicy(policy, sourceMetadata); + verifySource(sourceBytes, sourceMetadata) + verifyPolicy(policy, sourceMetadata) - const discovery = asRecord(JSON.parse(sourceBytes.toString("utf8"))); - if (discovery.version !== "v24" || discovery.revision !== sourceMetadata.revision) { - throw new Error("Pinned Google Ads discovery version or revision does not match metadata."); - } - const operations = collectOperations(discovery); - const operationsByKey = new Map( - operations.map((operation) => [operationKey(operation.path, operation.method), operation]), - ); - const reviewedByKey = reviewEntries(policy, operationsByKey); - const capabilities = policy.entries - .flatMap((entry) => entry.capabilities ?? []) - .sort((left, right) => String(left.id).localeCompare(String(right.id))); - const source = { - url: sourceMetadata.url, - revision: sourceMetadata.revision, - checksum: sourceMetadata.checksum, - }; - const manifest = stableJson({ - version: 1, - provider: policy.provider, - revision: policy.manifestRevision, - source, - reviewedAt: policy.reviewedAt, - capabilities, - }); - const compiledManifest = compileExecutableManifest(JSON.parse(manifest)); - const inventoryOperations = operations.map((operation): InventoryOperation => { - const reviewed = reviewedByKey.get(operationKey(operation.path, operation.method)); - if (!reviewed) { - return { - ...operation, - exposure: "inventory", - reason: "Not reviewed for executable gkit exposure.", - }; - } - if (reviewed.exposure === "inventory") { - return { ...operation, exposure: "inventory", reason: reviewed.reason! }; - } - return { - ...operation, - exposure: "executable", - capabilityIds: reviewed.capabilities!.map((capability) => String(capability.id)).sort(), - reason: - "Reviewed adapter, input, read effect, pagination, and response contracts are committed.", - }; - }); + const discovery = asRecord(JSON.parse(sourceBytes.toString('utf8'))) + if ( + discovery.version !== 'v24' || + discovery.revision !== sourceMetadata.revision + ) { + throw new Error( + 'Pinned Google Ads discovery version or revision does not match metadata.', + ) + } + const operations = collectOperations(discovery) + const operationsByKey = new Map( + operations.map((operation) => [ + operationKey(operation.path, operation.method), + operation, + ]), + ) + const reviewedByKey = reviewEntries(policy, operationsByKey) + const capabilities = policy.entries + .flatMap((entry) => entry.capabilities ?? []) + .sort((left, right) => String(left.id).localeCompare(String(right.id))) + const source = { + url: sourceMetadata.url, + revision: sourceMetadata.revision, + checksum: sourceMetadata.checksum, + } + const manifest = stableJson({ + version: 1, + provider: policy.provider, + revision: policy.manifestRevision, + source, + reviewedAt: policy.reviewedAt, + capabilities, + }) + const compiledManifest = compileExecutableManifest(JSON.parse(manifest)) + const inventoryOperations = operations.map( + (operation): InventoryOperation => { + const reviewed = reviewedByKey.get( + operationKey(operation.path, operation.method), + ) + if (!reviewed) { + return { + ...operation, + exposure: 'inventory', + reason: 'Not reviewed for executable gkit exposure.', + } + } + if (reviewed.exposure === 'inventory') { + return { ...operation, exposure: 'inventory', reason: reviewed.reason! } + } + return { + ...operation, + exposure: 'executable', + capabilityIds: reviewed + .capabilities!.map((capability) => String(capability.id)) + .sort(), + reason: + 'Reviewed adapter, input, read effect, pagination, and response contracts are committed.', + } + }, + ) - return { - manifest, - inventory: stableJson({ - version: 1, - provider: policy.provider, - revision: policy.manifestRevision, - source, - operations: inventoryOperations, - }), - docs: renderProviderDocs(compiledManifest), - inventoryDocs: renderInventoryDocs(policy, inventoryOperations), - }; + return { + manifest, + inventory: stableJson({ + version: 1, + provider: policy.provider, + revision: policy.manifestRevision, + source, + operations: inventoryOperations, + }), + docs: renderProviderDocs(compiledManifest), + inventoryDocs: renderInventoryDocs(policy, inventoryOperations), + } } async function run(): Promise { - const packageRoot = resolve(new URL("..", import.meta.url).pathname); - const check = process.argv.slice(2).includes("--check"); - const artifacts = await generateGoogleAdsArtifacts(packageRoot); - const outputs = [ - [join(packageRoot, "generated/google-ads/manifest.json"), artifacts.manifest], - [join(packageRoot, "generated/google-ads/inventory.json"), artifacts.inventory], - [join(packageRoot, "docs/providers/google-ads/capabilities.md"), artifacts.docs], - [join(packageRoot, "docs/providers/google-ads/inventory.md"), artifacts.inventoryDocs], - ] as const; + const packageRoot = resolve(new URL('..', import.meta.url).pathname) + const check = process.argv.slice(2).includes('--check') + const artifacts = await generateGoogleAdsArtifacts(packageRoot) + const outputs = [ + [ + join(packageRoot, 'generated/google-ads/manifest.json'), + artifacts.manifest, + ], + [ + join(packageRoot, 'generated/google-ads/inventory.json'), + artifacts.inventory, + ], + [ + join(packageRoot, 'docs/providers/google-ads/capabilities.md'), + artifacts.docs, + ], + [ + join(packageRoot, 'docs/providers/google-ads/inventory.md'), + artifacts.inventoryDocs, + ], + ] as const - if (check) { - const drifted: string[] = []; - for (const [path, expected] of outputs) { - try { - if ((await readFile(path, "utf8")) !== expected) drifted.push(path); - } catch { - drifted.push(path); - } - } - if (drifted.length > 0) { - throw new Error(`Generated Google Ads artifacts are stale: ${drifted.join(", ")}`); - } - return; - } + if (check) { + const drifted: string[] = [] + for (const [path, expected] of outputs) { + try { + if ((await readFile(path, 'utf8')) !== expected) drifted.push(path) + } catch { + drifted.push(path) + } + } + if (drifted.length > 0) { + throw new Error( + `Generated Google Ads artifacts are stale: ${drifted.join(', ')}`, + ) + } + return + } - for (const [path, contents] of outputs) { - await mkdir(dirname(path), { recursive: true }); - await writeFile(path, contents, "utf8"); - } + for (const [path, contents] of outputs) { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, contents, 'utf8') + } } function collectOperations(discovery: JsonRecord): InventoryOperation[] { - const operations: InventoryOperation[] = []; - walkResources(asRecord(discovery.resources), operations); - return operations.sort( - (left, right) => left.path.localeCompare(right.path) || left.method.localeCompare(right.method), - ); + const operations: InventoryOperation[] = [] + walkResources(asRecord(discovery.resources), operations) + return operations.sort( + (left, right) => + left.path.localeCompare(right.path) || + left.method.localeCompare(right.method), + ) } -function walkResources(resources: JsonRecord, operations: InventoryOperation[]): void { - for (const resourceValue of Object.values(resources)) { - const resource = asRecord(resourceValue); - const methods = asRecord(resource.methods ?? {}); - for (const methodValue of Object.values(methods)) { - const method = asRecord(methodValue); - if ( - typeof method.path !== "string" || - typeof method.httpMethod !== "string" || - typeof method.id !== "string" - ) { - throw new Error("Pinned Google Ads discovery contains an invalid REST method."); - } - operations.push({ - path: method.path, - method: method.httpMethod.toLowerCase(), - operationId: method.id, - exposure: "inventory", - reason: "", - }); - } - walkResources(asRecord(resource.resources ?? {}), operations); - } +function walkResources( + resources: JsonRecord, + operations: InventoryOperation[], +): void { + for (const resourceValue of Object.values(resources)) { + const resource = asRecord(resourceValue) + const methods = asRecord(resource.methods ?? {}) + for (const methodValue of Object.values(methods)) { + const method = asRecord(methodValue) + if ( + typeof method.path !== 'string' || + typeof method.httpMethod !== 'string' || + typeof method.id !== 'string' + ) { + throw new Error( + 'Pinned Google Ads discovery contains an invalid REST method.', + ) + } + operations.push({ + path: method.path, + method: method.httpMethod.toLowerCase(), + operationId: method.id, + exposure: 'inventory', + reason: '', + }) + } + walkResources(asRecord(resource.resources ?? {}), operations) + } } function reviewEntries( - policy: ReviewedPolicy, - operationsByKey: Map, + policy: ReviewedPolicy, + operationsByKey: Map, ): Map { - const reviewed = new Map(); - for (const entry of policy.entries) { - const key = operationKey(entry.path, entry.method); - if (reviewed.has(key)) throw new Error(`Duplicate reviewed Google Ads method ${key}.`); - const operation = operationsByKey.get(key); - if (!operation) throw new Error(`Reviewed Google Ads method ${key} is absent from source.`); - if (operation.operationId !== entry.operationId) { - throw new Error(`Reviewed Google Ads method ${key} changed operation ID.`); - } - if ( - entry.exposure === "executable" && - (!entry.capabilities || entry.capabilities.length === 0) - ) { - throw new Error(`Executable Google Ads method ${key} has no capability records.`); - } - if (entry.exposure === "inventory" && !entry.reason) { - throw new Error(`Inventory-only Google Ads method ${key} requires a reason.`); - } - reviewed.set(key, entry); - } - return reviewed; + const reviewed = new Map() + for (const entry of policy.entries) { + const key = operationKey(entry.path, entry.method) + if (reviewed.has(key)) + throw new Error(`Duplicate reviewed Google Ads method ${key}.`) + const operation = operationsByKey.get(key) + if (!operation) + throw new Error( + `Reviewed Google Ads method ${key} is absent from source.`, + ) + if (operation.operationId !== entry.operationId) { + throw new Error(`Reviewed Google Ads method ${key} changed operation ID.`) + } + if ( + entry.exposure === 'executable' && + (!entry.capabilities || entry.capabilities.length === 0) + ) { + throw new Error( + `Executable Google Ads method ${key} has no capability records.`, + ) + } + if (entry.exposure === 'inventory' && !entry.reason) { + throw new Error( + `Inventory-only Google Ads method ${key} requires a reason.`, + ) + } + reviewed.set(key, entry) + } + return reviewed } -function renderInventoryDocs(policy: ReviewedPolicy, operations: InventoryOperation[]): string { - const executableCount = operations.filter( - (operation) => operation.exposure === "executable", - ).length; - const lines = [ - "---", - "type: Reference", - "title: Google Ads REST operation inventory", - "description: >", - " Generated inventory of pinned Google Ads v24 REST methods and their gkit exposure decisions.", - `provider: ${policy.provider}`, - `inventoryRevision: ${policy.manifestRevision}`, - "---", - "", - "# Google Ads REST operation inventory", - "", - `This pinned inventory contains ${operations.length} methods: ${executableCount} executable and ${operations.length - executableCount} inventory-only.`, - "Inventory-only methods cannot be routed by `gkit google-ads api call`.", - "", - "| Method | Path | Operation ID | Exposure | Decision |", - "| --- | --- | --- | --- | --- |", - ]; - for (const operation of operations) { - const capabilities = operation.capabilityIds?.length - ? `; capabilities: ${operation.capabilityIds.map((id) => `\`${id}\``).join(", ")}` - : ""; - lines.push( - `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capabilities} |`, - ); - } - return `${lines.join("\n")}\n`; +function renderInventoryDocs( + policy: ReviewedPolicy, + operations: InventoryOperation[], +): string { + const executableCount = operations.filter( + (operation) => operation.exposure === 'executable', + ).length + const lines = [ + '---', + 'type: Reference', + 'title: Google Ads REST operation inventory', + 'description: >', + ' Generated inventory of pinned Google Ads v24 REST methods and their gkit exposure decisions.', + `provider: ${policy.provider}`, + `inventoryRevision: ${policy.manifestRevision}`, + '---', + '', + '# Google Ads REST operation inventory', + '', + `This pinned inventory contains ${operations.length} methods: ${executableCount} executable and ${operations.length - executableCount} inventory-only.`, + 'Inventory-only methods cannot be routed by `gkit google-ads api call`.', + '', + '| Method | Path | Operation ID | Exposure | Decision |', + '| --- | --- | --- | --- | --- |', + ] + for (const operation of operations) { + const capabilities = operation.capabilityIds?.length + ? `; capabilities: ${operation.capabilityIds.map((id) => `\`${id}\``).join(', ')}` + : '' + lines.push( + `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capabilities} |`, + ) + } + return `${lines.join('\n')}\n` } function verifySource(bytes: Uint8Array, metadata: SourceMetadata): void { - const checksum = `sha256:${createHash("sha256").update(bytes).digest("hex")}`; - if (checksum !== metadata.checksum) { - throw new Error(`Pinned Google Ads source checksum mismatch: expected ${metadata.checksum}.`); - } + const checksum = `sha256:${createHash('sha256').update(bytes).digest('hex')}` + if (checksum !== metadata.checksum) { + throw new Error( + `Pinned Google Ads source checksum mismatch: expected ${metadata.checksum}.`, + ) + } } function verifyPolicy(policy: ReviewedPolicy, metadata: SourceMetadata): void { - if ( - policy.version !== 1 || - policy.provider !== "google-ads" || - !policy.manifestRevision || - !policy.reviewedAt || - !Array.isArray(policy.entries) - ) { - throw new Error("Reviewed Google Ads policy is structurally invalid."); - } - if (policy.sourceChecksum !== metadata.checksum) { - throw new Error( - "Reviewed Google Ads policy approval does not match the pinned source checksum.", - ); - } + if ( + policy.version !== 1 || + policy.provider !== 'google-ads' || + !policy.manifestRevision || + !policy.reviewedAt || + !Array.isArray(policy.entries) + ) { + throw new Error('Reviewed Google Ads policy is structurally invalid.') + } + if (policy.sourceChecksum !== metadata.checksum) { + throw new Error( + 'Reviewed Google Ads policy approval does not match the pinned source checksum.', + ) + } } function operationKey(path: string, method: string): string { - return `${method.toLowerCase()} ${path}`; + return `${method.toLowerCase()} ${path}` } function stableJson(value: unknown): string { - return `${JSON.stringify(value, null, 2)}\n`; + return `${JSON.stringify(value, null, 2)}\n` } function asRecord(value: unknown): JsonRecord { - if (value === null || typeof value !== "object" || Array.isArray(value)) { - throw new Error("Pinned Google Ads discovery is not an object."); - } - return value as JsonRecord; + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error('Pinned Google Ads discovery is not an object.') + } + return value as JsonRecord } async function readJson(path: string): Promise { - return JSON.parse(await readFile(path, "utf8")) as T; + return JSON.parse(await readFile(path, 'utf8')) as T } -if (import.meta.main) await run(); +if (import.meta.main) await run() diff --git a/packages/gkit/scripts/generate-gsc.ts b/packages/gkit/scripts/generate-gsc.ts index e4343b2..ba6a464 100644 --- a/packages/gkit/scripts/generate-gsc.ts +++ b/packages/gkit/scripts/generate-gsc.ts @@ -1,11 +1,11 @@ -import { resolve } from "node:path"; +import { resolve } from 'node:path' -import { writeContractProviderArtifacts } from "./generate-contract-provider"; +import { writeContractProviderArtifacts } from './generate-contract-provider' if (import.meta.main) { - await writeContractProviderArtifacts({ - packageRoot: resolve(new URL("..", import.meta.url).pathname), - provider: "gsc", - check: process.argv.slice(2).includes("--check"), - }); + await writeContractProviderArtifacts({ + packageRoot: resolve(new URL('..', import.meta.url).pathname), + provider: 'gsc', + check: process.argv.slice(2).includes('--check'), + }) } diff --git a/packages/gkit/scripts/generate-hubspot.ts b/packages/gkit/scripts/generate-hubspot.ts index bac6044..1f535b2 100644 --- a/packages/gkit/scripts/generate-hubspot.ts +++ b/packages/gkit/scripts/generate-hubspot.ts @@ -1,11 +1,11 @@ -import { resolve } from "node:path"; +import { resolve } from 'node:path' -import { writeContractProviderArtifacts } from "./generate-contract-provider"; +import { writeContractProviderArtifacts } from './generate-contract-provider' if (import.meta.main) { - await writeContractProviderArtifacts({ - packageRoot: resolve(new URL("..", import.meta.url).pathname), - provider: "hubspot", - check: process.argv.slice(2).includes("--check"), - }); + await writeContractProviderArtifacts({ + packageRoot: resolve(new URL('..', import.meta.url).pathname), + provider: 'hubspot', + check: process.argv.slice(2).includes('--check'), + }) } diff --git a/packages/gkit/scripts/generate-pagespeed.ts b/packages/gkit/scripts/generate-pagespeed.ts index f552c85..2d1ef76 100644 --- a/packages/gkit/scripts/generate-pagespeed.ts +++ b/packages/gkit/scripts/generate-pagespeed.ts @@ -1,11 +1,11 @@ -import { resolve } from "node:path"; +import { resolve } from 'node:path' -import { writeContractProviderArtifacts } from "./generate-contract-provider"; +import { writeContractProviderArtifacts } from './generate-contract-provider' if (import.meta.main) { - await writeContractProviderArtifacts({ - packageRoot: resolve(new URL("..", import.meta.url).pathname), - provider: "pagespeed", - check: process.argv.slice(2).includes("--check"), - }); + await writeContractProviderArtifacts({ + packageRoot: resolve(new URL('..', import.meta.url).pathname), + provider: 'pagespeed', + check: process.argv.slice(2).includes('--check'), + }) } diff --git a/packages/gkit/scripts/generate-posthog.test.ts b/packages/gkit/scripts/generate-posthog.test.ts index 6ac2bb6..a620154 100644 --- a/packages/gkit/scripts/generate-posthog.test.ts +++ b/packages/gkit/scripts/generate-posthog.test.ts @@ -1,38 +1,38 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest' -import { generatePostHogArtifacts } from "./generate-posthog"; +import { generatePostHogArtifacts } from './generate-posthog' -const packageRoot = new URL("..", import.meta.url).pathname; +const packageRoot = new URL('..', import.meta.url).pathname -describe("PostHog artifact generator", () => { - it("projects the pinned query endpoint into one executable capability and inventory", async () => { - const first = await generatePostHogArtifacts(packageRoot); - const second = await generatePostHogArtifacts(packageRoot); +describe('PostHog artifact generator', () => { + it('projects the pinned query endpoint into one executable capability and inventory', async () => { + const first = await generatePostHogArtifacts(packageRoot) + const second = await generatePostHogArtifacts(packageRoot) - expect(second).toEqual(first); + expect(second).toEqual(first) - const manifest = JSON.parse(first.manifest) as { - capabilities: Array<{ id: string; adapterKey: string }>; - }; - const inventory = JSON.parse(first.inventory) as { - operations: Array<{ - operationId: string; - exposure: "executable" | "inventory"; - }>; - }; + const manifest = JSON.parse(first.manifest) as { + capabilities: Array<{ id: string; adapterKey: string }> + } + const inventory = JSON.parse(first.inventory) as { + operations: Array<{ + operationId: string + exposure: 'executable' | 'inventory' + }> + } - expect(manifest.capabilities).toEqual([ - expect.objectContaining({ - id: "posthog.query.run", - adapterKey: "query.run", - }), - ]); - expect(inventory.operations).toContainEqual( - expect.objectContaining({ - operationId: "query_create", - exposure: "executable", - }), - ); - expect(first.docs).toContain("posthog.query.run"); - }, 15_000); -}); + expect(manifest.capabilities).toEqual([ + expect.objectContaining({ + id: 'posthog.query.run', + adapterKey: 'query.run', + }), + ]) + expect(inventory.operations).toContainEqual( + expect.objectContaining({ + operationId: 'query_create', + exposure: 'executable', + }), + ) + expect(first.docs).toContain('posthog.query.run') + }, 15_000) +}) diff --git a/packages/gkit/scripts/generate-posthog.ts b/packages/gkit/scripts/generate-posthog.ts index ef2f124..145662c 100644 --- a/packages/gkit/scripts/generate-posthog.ts +++ b/packages/gkit/scripts/generate-posthog.ts @@ -1,297 +1,341 @@ -import { createHash } from "node:crypto"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; +import { createHash } from 'node:crypto' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join, resolve } from 'node:path' +import { parse } from 'yaml' -import { parse } from "yaml"; +import { renderProviderDocs } from '../src/docs' +import { compileExecutableManifest } from '../src/manifest' -import { renderProviderDocs } from "../src/docs"; -import { compileExecutableManifest } from "../src/manifest"; - -type JsonRecord = Record; +type JsonRecord = Record type SourceMetadata = { - url: string; - revision: string; - checksum: string; - retrievedAt: string; -}; + url: string + revision: string + checksum: string + retrievedAt: string +} type ReviewedEntry = { - path: string; - method: string; - operationId: string; - exposure: "executable" | "inventory"; - reason?: string; - capability?: JsonRecord; -}; + path: string + method: string + operationId: string + exposure: 'executable' | 'inventory' + reason?: string + capability?: JsonRecord +} type ReviewedPolicy = { - version: 1; - provider: "posthog"; - manifestRevision: string; - reviewedAt: string; - entries: ReviewedEntry[]; -}; + version: 1 + provider: 'posthog' + manifestRevision: string + reviewedAt: string + entries: ReviewedEntry[] +} type InventoryOperation = { - path: string; - method: string; - operationId: string; - tags: string[]; - exposure: "executable" | "inventory"; - capabilityId?: string; - reason: string; -}; + path: string + method: string + operationId: string + tags: string[] + exposure: 'executable' | 'inventory' + capabilityId?: string + reason: string +} export type GeneratedPostHogArtifacts = { - manifest: string; - inventory: string; - docs: string; - inventoryDocs: string; -}; + manifest: string + inventory: string + docs: string + inventoryDocs: string +} -const httpMethods = new Set(["delete", "get", "head", "options", "patch", "post", "put"]); +const httpMethods = new Set([ + 'delete', + 'get', + 'head', + 'options', + 'patch', + 'post', + 'put', +]) export async function generatePostHogArtifacts( - packageRoot: string, + packageRoot: string, ): Promise { - const sourceDirectory = join(packageRoot, "sources/posthog"); - const [sourceBytes, sourceMetadata, policy] = await Promise.all([ - readFile(join(sourceDirectory, "openapi.yaml")), - readJson(join(sourceDirectory, "source.json")), - readJson(join(packageRoot, "policy/posthog.reviewed.json")), - ]); + const sourceDirectory = join(packageRoot, 'sources/posthog') + const [sourceBytes, sourceMetadata, policy] = await Promise.all([ + readFile(join(sourceDirectory, 'openapi.yaml')), + readJson(join(sourceDirectory, 'source.json')), + readJson(join(packageRoot, 'policy/posthog.reviewed.json')), + ]) - verifySource(sourceBytes, sourceMetadata); - verifyPolicy(policy); + verifySource(sourceBytes, sourceMetadata) + verifyPolicy(policy) - const openApi = asRecord(parse(sourceBytes.toString("utf8"))); - const operations = collectOperations(openApi); - const operationsByKey = new Map( - operations.map((operation) => [operationKey(operation.path, operation.method), operation]), - ); - const reviewedByKey = reviewEntries(policy, operationsByKey); - const capabilities = policy.entries - .filter((entry) => entry.exposure === "executable") - .map((entry) => entry.capability!) - .sort((left, right) => String(left.id).localeCompare(String(right.id))); - const source = { - url: sourceMetadata.url, - revision: sourceMetadata.revision, - checksum: sourceMetadata.checksum, - }; - const manifest = stableJson({ - version: 1, - provider: policy.provider, - revision: policy.manifestRevision, - source, - reviewedAt: policy.reviewedAt, - capabilities, - }); - const compiledManifest = compileExecutableManifest(JSON.parse(manifest)); - const inventoryOperations = operations.map((operation): InventoryOperation => { - const reviewed = reviewedByKey.get(operationKey(operation.path, operation.method)); - if (!reviewed) { - return { - ...operation, - exposure: "inventory", - reason: "Not reviewed for executable gkit exposure.", - }; - } - if (reviewed.exposure === "inventory") { - return { - ...operation, - exposure: "inventory", - reason: reviewed.reason!, - }; - } - return { - ...operation, - exposure: "executable", - capabilityId: String(reviewed.capability!.id), - reason: "Reviewed adapter, input, effect, and response contracts are committed.", - }; - }); + const openApi = asRecord(parse(sourceBytes.toString('utf8'))) + const operations = collectOperations(openApi) + const operationsByKey = new Map( + operations.map((operation) => [ + operationKey(operation.path, operation.method), + operation, + ]), + ) + const reviewedByKey = reviewEntries(policy, operationsByKey) + const capabilities = policy.entries + .filter((entry) => entry.exposure === 'executable') + .map((entry) => entry.capability!) + .sort((left, right) => String(left.id).localeCompare(String(right.id))) + const source = { + url: sourceMetadata.url, + revision: sourceMetadata.revision, + checksum: sourceMetadata.checksum, + } + const manifest = stableJson({ + version: 1, + provider: policy.provider, + revision: policy.manifestRevision, + source, + reviewedAt: policy.reviewedAt, + capabilities, + }) + const compiledManifest = compileExecutableManifest(JSON.parse(manifest)) + const inventoryOperations = operations.map( + (operation): InventoryOperation => { + const reviewed = reviewedByKey.get( + operationKey(operation.path, operation.method), + ) + if (!reviewed) { + return { + ...operation, + exposure: 'inventory', + reason: 'Not reviewed for executable gkit exposure.', + } + } + if (reviewed.exposure === 'inventory') { + return { + ...operation, + exposure: 'inventory', + reason: reviewed.reason!, + } + } + return { + ...operation, + exposure: 'executable', + capabilityId: String(reviewed.capability!.id), + reason: + 'Reviewed adapter, input, effect, and response contracts are committed.', + } + }, + ) - return { - manifest, - inventory: stableJson({ - version: 1, - provider: policy.provider, - revision: policy.manifestRevision, - source, - operations: inventoryOperations, - }), - docs: renderProviderDocs(compiledManifest), - inventoryDocs: renderInventoryDocs(policy, inventoryOperations), - }; + return { + manifest, + inventory: stableJson({ + version: 1, + provider: policy.provider, + revision: policy.manifestRevision, + source, + operations: inventoryOperations, + }), + docs: renderProviderDocs(compiledManifest), + inventoryDocs: renderInventoryDocs(policy, inventoryOperations), + } } async function run(): Promise { - const packageRoot = resolve(new URL("..", import.meta.url).pathname); - const check = process.argv.slice(2).includes("--check"); - const artifacts = await generatePostHogArtifacts(packageRoot); - const outputs = [ - [join(packageRoot, "generated/posthog/manifest.json"), artifacts.manifest], - [join(packageRoot, "generated/posthog/inventory.json"), artifacts.inventory], - [join(packageRoot, "docs/providers/posthog/capabilities.md"), artifacts.docs], - [join(packageRoot, "docs/providers/posthog/inventory.md"), artifacts.inventoryDocs], - ] as const; + const packageRoot = resolve(new URL('..', import.meta.url).pathname) + const check = process.argv.slice(2).includes('--check') + const artifacts = await generatePostHogArtifacts(packageRoot) + const outputs = [ + [join(packageRoot, 'generated/posthog/manifest.json'), artifacts.manifest], + [ + join(packageRoot, 'generated/posthog/inventory.json'), + artifacts.inventory, + ], + [ + join(packageRoot, 'docs/providers/posthog/capabilities.md'), + artifacts.docs, + ], + [ + join(packageRoot, 'docs/providers/posthog/inventory.md'), + artifacts.inventoryDocs, + ], + ] as const - if (check) { - const drifted: string[] = []; - for (const [path, expected] of outputs) { - try { - if ((await readFile(path, "utf8")) !== expected) drifted.push(path); - } catch { - drifted.push(path); - } - } - if (drifted.length > 0) { - throw new Error(`Generated PostHog artifacts are stale: ${drifted.join(", ")}`); - } - return; - } + if (check) { + const drifted: string[] = [] + for (const [path, expected] of outputs) { + try { + if ((await readFile(path, 'utf8')) !== expected) drifted.push(path) + } catch { + drifted.push(path) + } + } + if (drifted.length > 0) { + throw new Error( + `Generated PostHog artifacts are stale: ${drifted.join(', ')}`, + ) + } + return + } - for (const [path, contents] of outputs) { - await mkdir(dirname(path), { recursive: true }); - await writeFile(path, contents, "utf8"); - } + for (const [path, contents] of outputs) { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, contents, 'utf8') + } } function reviewEntries( - policy: ReviewedPolicy, - operationsByKey: Map, + policy: ReviewedPolicy, + operationsByKey: Map, ): Map { - const reviewedByKey = new Map(); - for (const entry of policy.entries) { - const key = operationKey(entry.path, entry.method); - if (reviewedByKey.has(key)) { - throw new Error(`Reviewed PostHog policy contains duplicate operation ${key}.`); - } - const operation = operationsByKey.get(key); - if (!operation) { - throw new Error(`Reviewed PostHog operation ${key} is absent from the pinned source.`); - } - if (operation.operationId !== entry.operationId) { - throw new Error( - `Reviewed operation ${key} expected ${entry.operationId}, found ${operation.operationId}.`, - ); - } - if (entry.exposure === "executable" && !entry.capability) { - throw new Error(`Executable PostHog operation ${key} has no capability record.`); - } - if (entry.exposure === "inventory" && !entry.reason) { - throw new Error(`Inventory-only PostHog operation ${key} requires a reason.`); - } - reviewedByKey.set(key, entry); - } - return reviewedByKey; + const reviewedByKey = new Map() + for (const entry of policy.entries) { + const key = operationKey(entry.path, entry.method) + if (reviewedByKey.has(key)) { + throw new Error( + `Reviewed PostHog policy contains duplicate operation ${key}.`, + ) + } + const operation = operationsByKey.get(key) + if (!operation) { + throw new Error( + `Reviewed PostHog operation ${key} is absent from the pinned source.`, + ) + } + if (operation.operationId !== entry.operationId) { + throw new Error( + `Reviewed operation ${key} expected ${entry.operationId}, found ${operation.operationId}.`, + ) + } + if (entry.exposure === 'executable' && !entry.capability) { + throw new Error( + `Executable PostHog operation ${key} has no capability record.`, + ) + } + if (entry.exposure === 'inventory' && !entry.reason) { + throw new Error( + `Inventory-only PostHog operation ${key} requires a reason.`, + ) + } + reviewedByKey.set(key, entry) + } + return reviewedByKey } function collectOperations(openApi: JsonRecord): InventoryOperation[] { - const paths = asRecord(openApi.paths); - return Object.entries(paths) - .flatMap(([path, pathValue]) => { - const pathRecord = asRecord(pathValue); - return Object.entries(pathRecord) - .filter(([method]) => httpMethods.has(method.toLowerCase())) - .map(([method, operationValue]) => { - const operation = asRecord(operationValue); - const operationId = operation.operationId; - if (typeof operationId !== "string" || operationId.length === 0) { - throw new Error( - `Pinned PostHog operation ${method.toUpperCase()} ${path} has no operationId.`, - ); - } - const tags = Array.isArray(operation.tags) - ? operation.tags.filter((tag): tag is string => typeof tag === "string").sort() - : []; - return { - path, - method: method.toLowerCase(), - operationId, - tags, - exposure: "inventory" as const, - reason: "", - }; - }); - }) - .sort( - (left, right) => - left.path.localeCompare(right.path) || left.method.localeCompare(right.method), - ); + const paths = asRecord(openApi.paths) + return Object.entries(paths) + .flatMap(([path, pathValue]) => { + const pathRecord = asRecord(pathValue) + return Object.entries(pathRecord) + .filter(([method]) => httpMethods.has(method.toLowerCase())) + .map(([method, operationValue]) => { + const operation = asRecord(operationValue) + const operationId = operation.operationId + if (typeof operationId !== 'string' || operationId.length === 0) { + throw new Error( + `Pinned PostHog operation ${method.toUpperCase()} ${path} has no operationId.`, + ) + } + const tags = Array.isArray(operation.tags) + ? operation.tags + .filter((tag): tag is string => typeof tag === 'string') + .sort() + : [] + return { + path, + method: method.toLowerCase(), + operationId, + tags, + exposure: 'inventory' as const, + reason: '', + } + }) + }) + .sort( + (left, right) => + left.path.localeCompare(right.path) || + left.method.localeCompare(right.method), + ) } -function renderInventoryDocs(policy: ReviewedPolicy, operations: InventoryOperation[]): string { - const executableCount = operations.filter( - (operation) => operation.exposure === "executable", - ).length; - const lines = [ - "---", - "type: Reference", - "title: PostHog operation inventory", - "description: >", - " Generated inventory of pinned PostHog operations and their gkit exposure decisions.", - `provider: ${policy.provider}`, - `inventoryRevision: ${policy.manifestRevision}`, - "---", - "", - "# PostHog operation inventory", - "", - `This pinned inventory contains ${operations.length} operations: ${executableCount} executable and ${operations.length - executableCount} inventory-only.`, - "Inventory-only operations cannot be routed by `gkit posthog api call`.", - "", - "| Method | Path | Operation ID | Exposure | Decision |", - "| --- | --- | --- | --- | --- |", - ]; - for (const operation of operations) { - const capability = operation.capabilityId ? `; capability: \`${operation.capabilityId}\`` : ""; - lines.push( - `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capability} |`, - ); - } - return `${lines.join("\n")}\n`; +function renderInventoryDocs( + policy: ReviewedPolicy, + operations: InventoryOperation[], +): string { + const executableCount = operations.filter( + (operation) => operation.exposure === 'executable', + ).length + const lines = [ + '---', + 'type: Reference', + 'title: PostHog operation inventory', + 'description: >', + ' Generated inventory of pinned PostHog operations and their gkit exposure decisions.', + `provider: ${policy.provider}`, + `inventoryRevision: ${policy.manifestRevision}`, + '---', + '', + '# PostHog operation inventory', + '', + `This pinned inventory contains ${operations.length} operations: ${executableCount} executable and ${operations.length - executableCount} inventory-only.`, + 'Inventory-only operations cannot be routed by `gkit posthog api call`.', + '', + '| Method | Path | Operation ID | Exposure | Decision |', + '| --- | --- | --- | --- | --- |', + ] + for (const operation of operations) { + const capability = operation.capabilityId + ? `; capability: \`${operation.capabilityId}\`` + : '' + lines.push( + `| \`${operation.method.toUpperCase()}\` | \`${operation.path}\` | \`${operation.operationId}\` | \`${operation.exposure}\` | ${operation.reason}${capability} |`, + ) + } + return `${lines.join('\n')}\n` } function verifySource(bytes: Uint8Array, metadata: SourceMetadata): void { - const checksum = `sha256:${createHash("sha256").update(bytes).digest("hex")}`; - if (checksum !== metadata.checksum) { - throw new Error(`Pinned PostHog source checksum mismatch: expected ${metadata.checksum}.`); - } + const checksum = `sha256:${createHash('sha256').update(bytes).digest('hex')}` + if (checksum !== metadata.checksum) { + throw new Error( + `Pinned PostHog source checksum mismatch: expected ${metadata.checksum}.`, + ) + } } function verifyPolicy(policy: ReviewedPolicy): void { - if ( - policy.version !== 1 || - policy.provider !== "posthog" || - !policy.manifestRevision || - !policy.reviewedAt || - !Array.isArray(policy.entries) - ) { - throw new Error("Reviewed PostHog policy is structurally invalid."); - } + if ( + policy.version !== 1 || + policy.provider !== 'posthog' || + !policy.manifestRevision || + !policy.reviewedAt || + !Array.isArray(policy.entries) + ) { + throw new Error('Reviewed PostHog policy is structurally invalid.') + } } function operationKey(path: string, method: string): string { - return `${method.toLowerCase()} ${path}`; + return `${method.toLowerCase()} ${path}` } function stableJson(value: unknown): string { - return `${JSON.stringify(value, null, 2)}\n`; + return `${JSON.stringify(value, null, 2)}\n` } function asRecord(value: unknown): JsonRecord { - if (value === null || typeof value !== "object" || Array.isArray(value)) { - throw new Error("Pinned PostHog source is not an OpenAPI object."); - } - return value as JsonRecord; + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error('Pinned PostHog source is not an OpenAPI object.') + } + return value as JsonRecord } async function readJson(path: string): Promise { - return JSON.parse(await readFile(path, "utf8")) as T; + return JSON.parse(await readFile(path, 'utf8')) as T } if (import.meta.main) { - await run(); + await run() } diff --git a/packages/gkit/scripts/release-preflight.test.ts b/packages/gkit/scripts/release-preflight.test.ts index 9a26b26..62ffea0 100644 --- a/packages/gkit/scripts/release-preflight.test.ts +++ b/packages/gkit/scripts/release-preflight.test.ts @@ -1,62 +1,72 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest' -import { compareSemVer, decideRelease, parseSemVer } from "./release-preflight"; +import { compareSemVer, decideRelease, parseSemVer } from './release-preflight' -describe("release preflight", () => { - it("implements SemVer precedence including prereleases", () => { - const ordered = [ - "1.0.0-alpha", - "1.0.0-alpha.1", - "1.0.0-alpha.beta", - "1.0.0-beta", - "1.0.0-beta.2", - "1.0.0-beta.11", - "1.0.0-rc.1", - "1.0.0", - ].map(parseSemVer); - for (let index = 1; index < ordered.length; index += 1) { - expect(compareSemVer(ordered[index - 1]!, ordered[index]!)).toBe(-1); - } - expect( - compareSemVer( - parseSemVer("999999999999999999999999.0.0"), - parseSemVer("1000000000000000000000000.0.0"), - ), - ).toBe(-1); - }); +describe('release preflight', () => { + it('implements SemVer precedence including prereleases', () => { + const ordered = [ + '1.0.0-alpha', + '1.0.0-alpha.1', + '1.0.0-alpha.beta', + '1.0.0-beta', + '1.0.0-beta.2', + '1.0.0-beta.11', + '1.0.0-rc.1', + '1.0.0', + ].map(parseSemVer) + for (let index = 1; index < ordered.length; index += 1) { + expect(compareSemVer(ordered[index - 1]!, ordered[index]!)).toBe(-1) + } + expect( + compareSemVer( + parseSemVer('999999999999999999999999.0.0'), + parseSemVer('1000000000000000000000000.0.0'), + ), + ).toBe(-1) + }) - it("rejects invalid SemVer values", () => { - for (const version of ["v1.0.0", "1.0", "01.0.0", "1.0.0-01", "1.0.0-"]) { - expect(() => parseSemVer(version)).toThrow("Invalid SemVer"); - } - }); + it('rejects invalid SemVer values', () => { + for (const version of ['v1.0.0', '1.0', '01.0.0', '1.0.0-01', '1.0.0-']) { + expect(() => parseSemVer(version)).toThrow('Invalid SemVer') + } + }) - it("allows a new version only when it is greater than every release tag", () => { - expect(decideRelease("1.2.0", ["not-a-release", "v1.1.9"], "abc", new Map())).toEqual({ - version: "1.2.0", - tag: "v1.2.0", - prerelease: false, - tagExists: false, - }); - expect(() => decideRelease("1.1.8", ["v1.1.9"], "abc", new Map())).toThrow( - "must be greater", - ); - }); + it('allows a new version only when it is greater than every release tag', () => { + expect( + decideRelease('1.2.0', ['not-a-release', 'v1.1.9'], 'abc', new Map()), + ).toEqual({ + version: '1.2.0', + tag: 'v1.2.0', + prerelease: false, + tagExists: false, + }) + expect(() => decideRelease('1.1.8', ['v1.1.9'], 'abc', new Map())).toThrow( + 'must be greater', + ) + }) - it("permits an idempotent rerun only when the tag points to the same commit", () => { - expect(decideRelease("1.2.0-beta.1", ["v1.2.0-beta.1"], "abc", new Map([ - ["v1.2.0-beta.1", "abc"], - ]))).toMatchObject({ prerelease: true, tagExists: true }); - expect(() => - decideRelease("1.2.0-beta.1", ["v1.2.0-beta.1"], "abc", new Map([ - ["v1.2.0-beta.1", "different"], - ])), - ).toThrow("not abc"); - }); + it('permits an idempotent rerun only when the tag points to the same commit', () => { + expect( + decideRelease( + '1.2.0-beta.1', + ['v1.2.0-beta.1'], + 'abc', + new Map([['v1.2.0-beta.1', 'abc']]), + ), + ).toMatchObject({ prerelease: true, tagExists: true }) + expect(() => + decideRelease( + '1.2.0-beta.1', + ['v1.2.0-beta.1'], + 'abc', + new Map([['v1.2.0-beta.1', 'different']]), + ), + ).toThrow('not abc') + }) - it("does not let build metadata bypass equal precedence", () => { - expect(() => decideRelease("1.2.0+new", ["v1.2.0+old"], "abc", new Map())).toThrow( - "must be greater", - ); - }); -}); + it('does not let build metadata bypass equal precedence', () => { + expect(() => + decideRelease('1.2.0+new', ['v1.2.0+old'], 'abc', new Map()), + ).toThrow('must be greater') + }) +}) diff --git a/packages/gkit/scripts/release-preflight.ts b/packages/gkit/scripts/release-preflight.ts index e104bf3..44909c3 100644 --- a/packages/gkit/scripts/release-preflight.ts +++ b/packages/gkit/scripts/release-preflight.ts @@ -1,241 +1,265 @@ -import { appendFile, readFile } from "node:fs/promises"; -import { resolve } from "node:path"; +import { appendFile, readFile } from 'node:fs/promises' +import { resolve } from 'node:path' type SemVer = Readonly<{ - raw: string; - major: bigint; - minor: bigint; - patch: bigint; - prerelease: readonly string[]; -}>; + raw: string + major: bigint + minor: bigint + patch: bigint + prerelease: readonly string[] +}> type ReleaseDecision = Readonly<{ - version: string; - tag: string; - prerelease: boolean; - tagExists: boolean; -}>; + version: string + tag: string + prerelease: boolean + tagExists: boolean +}> type CommandResult = Readonly<{ - stdout: string; - stderr: string; -}>; + stdout: string + stderr: string +}> -const packagePath = "packages/gkit/package.json"; +const packagePath = 'packages/gkit/package.json' const semVerPattern = - /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/ export function parseSemVer(value: string): SemVer { - const match = semVerPattern.exec(value); - if (!match) { - throw new Error(`Invalid SemVer version: ${value}`); - } - return Object.freeze({ - raw: value, - major: BigInt(match[1]), - minor: BigInt(match[2]), - patch: BigInt(match[3]), - prerelease: Object.freeze(match[4]?.split(".") ?? []), - }); + const match = semVerPattern.exec(value) + if (!match) { + throw new Error(`Invalid SemVer version: ${value}`) + } + return Object.freeze({ + raw: value, + major: BigInt(match[1]), + minor: BigInt(match[2]), + patch: BigInt(match[3]), + prerelease: Object.freeze(match[4]?.split('.') ?? []), + }) } export function compareSemVer(left: SemVer, right: SemVer): number { - for (const key of ["major", "minor", "patch"] as const) { - if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1; - } - if (left.prerelease.length === 0 || right.prerelease.length === 0) { - if (left.prerelease.length === right.prerelease.length) return 0; - return left.prerelease.length === 0 ? 1 : -1; - } - const length = Math.max(left.prerelease.length, right.prerelease.length); - for (let index = 0; index < length; index += 1) { - const leftIdentifier = left.prerelease[index]; - const rightIdentifier = right.prerelease[index]; - if (leftIdentifier === rightIdentifier) continue; - if (leftIdentifier === undefined) return -1; - if (rightIdentifier === undefined) return 1; - const leftNumeric = /^\d+$/.test(leftIdentifier); - const rightNumeric = /^\d+$/.test(rightIdentifier); - if (leftNumeric && rightNumeric) { - if (leftIdentifier.length !== rightIdentifier.length) { - return leftIdentifier.length < rightIdentifier.length ? -1 : 1; - } - return leftIdentifier < rightIdentifier ? -1 : 1; - } - if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1; - return leftIdentifier < rightIdentifier ? -1 : 1; - } - return 0; + for (const key of ['major', 'minor', 'patch'] as const) { + if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1 + } + if (left.prerelease.length === 0 || right.prerelease.length === 0) { + if (left.prerelease.length === right.prerelease.length) return 0 + return left.prerelease.length === 0 ? 1 : -1 + } + const length = Math.max(left.prerelease.length, right.prerelease.length) + for (let index = 0; index < length; index += 1) { + const leftIdentifier = left.prerelease[index] + const rightIdentifier = right.prerelease[index] + if (leftIdentifier === rightIdentifier) continue + if (leftIdentifier === undefined) return -1 + if (rightIdentifier === undefined) return 1 + const leftNumeric = /^\d+$/.test(leftIdentifier) + const rightNumeric = /^\d+$/.test(rightIdentifier) + if (leftNumeric && rightNumeric) { + if (leftIdentifier.length !== rightIdentifier.length) { + return leftIdentifier.length < rightIdentifier.length ? -1 : 1 + } + return leftIdentifier < rightIdentifier ? -1 : 1 + } + if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1 + return leftIdentifier < rightIdentifier ? -1 : 1 + } + return 0 } export function decideRelease( - versionValue: string, - tags: readonly string[], - targetSha: string, - tagTargets: ReadonlyMap, + versionValue: string, + tags: readonly string[], + targetSha: string, + tagTargets: ReadonlyMap, ): ReleaseDecision { - const version = parseSemVer(versionValue); - const tag = `v${version.raw}`; - const releaseTags = tags.flatMap((candidate) => { - if (!candidate.startsWith("v")) return []; - try { - return [{ tag: candidate, version: parseSemVer(candidate.slice(1)) }]; - } catch { - return []; - } - }); - const existingTarget = tagTargets.get(tag); - if (existingTarget !== undefined && existingTarget !== targetSha) { - throw new Error(`Existing tag ${tag} points to ${existingTarget}, not ${targetSha}.`); - } - for (const existing of releaseTags) { - if (existing.tag === tag) continue; - if (compareSemVer(version, existing.version) <= 0) { - throw new Error( - `Version ${version.raw} must be greater than existing release tag ${existing.tag}.`, - ); - } - } - return Object.freeze({ - version: version.raw, - tag, - prerelease: version.prerelease.length > 0, - tagExists: existingTarget !== undefined, - }); + const version = parseSemVer(versionValue) + const tag = `v${version.raw}` + const releaseTags = tags.flatMap((candidate) => { + if (!candidate.startsWith('v')) return [] + try { + return [{ tag: candidate, version: parseSemVer(candidate.slice(1)) }] + } catch { + return [] + } + }) + const existingTarget = tagTargets.get(tag) + if (existingTarget !== undefined && existingTarget !== targetSha) { + throw new Error( + `Existing tag ${tag} points to ${existingTarget}, not ${targetSha}.`, + ) + } + for (const existing of releaseTags) { + if (existing.tag === tag) continue + if (compareSemVer(version, existing.version) <= 0) { + throw new Error( + `Version ${version.raw} must be greater than existing release tag ${existing.tag}.`, + ) + } + } + return Object.freeze({ + version: version.raw, + tag, + prerelease: version.prerelease.length > 0, + tagExists: existingTarget !== undefined, + }) } -if (import.meta.main) await main(); +if (import.meta.main) await main() async function main(): Promise { - const options = parseOptions(Bun.argv.slice(2)); - if (options.ref !== "refs/heads/main") { - throw new Error(`Releases must run from refs/heads/main, received ${options.ref}.`); - } - const packageJson = await readPackageJson(resolve(packagePath)); - const version = requireVersion(packageJson); - if (options.eventName === "push" && options.beforeSha !== undefined) { - const previousPackageJson = await readPackageJsonAtCommit(options.beforeSha); - if (previousPackageJson !== undefined && requireVersion(previousPackageJson) === version) { - await writeOutputs(options.output, { release: "false" }); - process.stdout.write("Package version did not change; release skipped.\n"); - return; - } - } - const tags = (await runCommand(["git", "tag", "--list"], process.cwd())).stdout - .split("\n") - .filter(Boolean); - const tagTargets = new Map(); - for (const tag of tags) { - const target = ( - await runCommand(["git", "rev-list", "-n", "1", tag], process.cwd()) - ).stdout.trim(); - tagTargets.set(tag, target); - } - const decision = decideRelease(version, tags, options.targetSha, tagTargets); - await writeOutputs(options.output, { - release: "true", - version: decision.version, - tag: decision.tag, - prerelease: String(decision.prerelease), - tag_exists: String(decision.tagExists), - }); - process.stdout.write( - `Release preflight passed for ${decision.tag} at ${options.targetSha}.\n`, - ); + const options = parseOptions(Bun.argv.slice(2)) + if (options.ref !== 'refs/heads/main') { + throw new Error( + `Releases must run from refs/heads/main, received ${options.ref}.`, + ) + } + const packageJson = await readPackageJson(resolve(packagePath)) + const version = requireVersion(packageJson) + if (options.eventName === 'push' && options.beforeSha !== undefined) { + const previousPackageJson = await readPackageJsonAtCommit(options.beforeSha) + if ( + previousPackageJson !== undefined && + requireVersion(previousPackageJson) === version + ) { + await writeOutputs(options.output, { release: 'false' }) + process.stdout.write('Package version did not change; release skipped.\n') + return + } + } + const tags = ( + await runCommand(['git', 'tag', '--list'], process.cwd()) + ).stdout + .split('\n') + .filter(Boolean) + const tagTargets = new Map() + for (const tag of tags) { + const target = ( + await runCommand(['git', 'rev-list', '-n', '1', tag], process.cwd()) + ).stdout.trim() + tagTargets.set(tag, target) + } + const decision = decideRelease(version, tags, options.targetSha, tagTargets) + await writeOutputs(options.output, { + release: 'true', + version: decision.version, + tag: decision.tag, + prerelease: String(decision.prerelease), + tag_exists: String(decision.tagExists), + }) + process.stdout.write( + `Release preflight passed for ${decision.tag} at ${options.targetSha}.\n`, + ) } function parseOptions(arguments_: readonly string[]): { - eventName: string; - beforeSha?: string; - targetSha: string; - ref: string; - output: string; + eventName: string + beforeSha?: string + targetSha: string + ref: string + output: string } { - const values = new Map(); - for (let index = 0; index < arguments_.length; index += 2) { - const key = arguments_[index]; - const value = arguments_[index + 1]; - if (!key?.startsWith("--") || value === undefined) { - throw new Error("Release preflight arguments must be --key value pairs."); - } - values.set(key.slice(2), value); - } - const eventName = requireOption(values, "event-name"); - const targetSha = requireOption(values, "target-sha"); - const ref = requireOption(values, "ref"); - const output = requireOption(values, "output"); - const beforeSha = values.get("before-sha"); - return { eventName, targetSha, ref, output, ...(beforeSha ? { beforeSha } : {}) }; + const values = new Map() + for (let index = 0; index < arguments_.length; index += 2) { + const key = arguments_[index] + const value = arguments_[index + 1] + if (!key?.startsWith('--') || value === undefined) { + throw new Error('Release preflight arguments must be --key value pairs.') + } + values.set(key.slice(2), value) + } + const eventName = requireOption(values, 'event-name') + const targetSha = requireOption(values, 'target-sha') + const ref = requireOption(values, 'ref') + const output = requireOption(values, 'output') + const beforeSha = values.get('before-sha') + return { + eventName, + targetSha, + ref, + output, + ...(beforeSha ? { beforeSha } : {}), + } } -function requireOption(values: ReadonlyMap, name: string): string { - const value = values.get(name); - if (!value) throw new Error(`Missing required option --${name}.`); - return value; +function requireOption( + values: ReadonlyMap, + name: string, +): string { + const value = values.get(name) + if (!value) throw new Error(`Missing required option --${name}.`) + return value } async function readPackageJson(path: string): Promise { - return JSON.parse(await readFile(path, "utf8")) as unknown; + return JSON.parse(await readFile(path, 'utf8')) as unknown } -async function readPackageJsonAtCommit(commit: string): Promise { - if (/^0+$/.test(commit)) return undefined; - const result = await runCommand( - ["git", "show", `${commit}:${packagePath}`], - process.cwd(), - true, - ); - if (!result) return undefined; - return JSON.parse(result.stdout) as unknown; +async function readPackageJsonAtCommit( + commit: string, +): Promise { + if (/^0+$/.test(commit)) return undefined + const result = await runCommand( + ['git', 'show', `${commit}:${packagePath}`], + process.cwd(), + true, + ) + if (!result) return undefined + return JSON.parse(result.stdout) as unknown } function requireVersion(value: unknown): string { - if (!value || typeof value !== "object" || !("version" in value)) { - throw new Error(`${packagePath} must contain a version.`); - } - const version = (value as { version?: unknown }).version; - if (typeof version !== "string") throw new Error("Package version must be a string."); - parseSemVer(version); - return version; + if (!value || typeof value !== 'object' || !('version' in value)) { + throw new Error(`${packagePath} must contain a version.`) + } + const version = (value as { version?: unknown }).version + if (typeof version !== 'string') + throw new Error('Package version must be a string.') + parseSemVer(version) + return version } async function writeOutputs( - outputPath: string, - values: Readonly>, + outputPath: string, + values: Readonly>, ): Promise { - const content = Object.entries(values) - .map(([key, value]) => `${key}=${value}\n`) - .join(""); - await appendFile(outputPath, content, "utf8"); + const content = Object.entries(values) + .map(([key, value]) => `${key}=${value}\n`) + .join('') + await appendFile(outputPath, content, 'utf8') } -function runCommand(command: readonly string[], cwd: string): Promise; function runCommand( - command: readonly string[], - cwd: string, - allowFailure: true, -): Promise; + command: readonly string[], + cwd: string, +): Promise +function runCommand( + command: readonly string[], + cwd: string, + allowFailure: true, +): Promise async function runCommand( - command: readonly string[], - cwd: string, - allowFailure = false, + command: readonly string[], + cwd: string, + allowFailure = false, ): Promise { - const child = Bun.spawn([...command], { - cwd, - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - }); - const [exitCode, stdout, stderr] = await Promise.all([ - child.exited, - new Response(child.stdout).text(), - new Response(child.stderr).text(), - ]); - if (exitCode !== 0) { - if (allowFailure) return undefined; - throw new Error( - `Command failed (${exitCode}): ${command.join(" ")}\n${stdout}${stderr}`.trimEnd(), - ); - } - return { stdout, stderr }; + const child = Bun.spawn([...command], { + cwd, + stdin: 'ignore', + stdout: 'pipe', + stderr: 'pipe', + }) + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]) + if (exitCode !== 0) { + if (allowFailure) return undefined + throw new Error( + `Command failed (${exitCode}): ${command.join(' ')}\n${stdout}${stderr}`.trimEnd(), + ) + } + return { stdout, stderr } } diff --git a/packages/gkit/scripts/test-site-snapshot.ts b/packages/gkit/scripts/test-site-snapshot.ts index 6f9f5f9..4e2b39e 100644 --- a/packages/gkit/scripts/test-site-snapshot.ts +++ b/packages/gkit/scripts/test-site-snapshot.ts @@ -1,322 +1,364 @@ -import assert from "node:assert/strict"; -import { mkdtemp, mkdir, writeFile, readFile, readdir, rm, cp } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; -import { readSnapshot } from "../src/site/snapshot"; -const exec = promisify(execFile); -const root = await mkdtemp(join(tmpdir(), "gkit-snapshot-test-")); -const repo = join(root, "repo"); -await mkdir(repo); -const git = async (args: string[]) => (await exec("git", ["-C", repo, ...args])).stdout.trim(); -const cli = new URL("../src/cli.ts", import.meta.url).pathname; -const config = join(root, "config"); -const profile = join(config, "gkit/profiles/app"); -await mkdir(profile, { recursive: true }); +import assert from 'node:assert/strict' +import { execFile } from 'node:child_process' +import { + mkdtemp, + mkdir, + writeFile, + readFile, + readdir, + rm, + cp, +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { promisify } from 'node:util' + +import { readSnapshot } from '../src/site/snapshot' +const exec = promisify(execFile) +const root = await mkdtemp(join(tmpdir(), 'gkit-snapshot-test-')) +const repo = join(root, 'repo') +await mkdir(repo) +const git = async (args: string[]) => + (await exec('git', ['-C', repo, ...args])).stdout.trim() +const cli = new URL('../src/cli.ts', import.meta.url).pathname +const config = join(root, 'config') +const profile = join(config, 'gkit/profiles/app') +await mkdir(profile, { recursive: true }) await writeFile( - join(config, "gkit/profiles/app.json"), - JSON.stringify({ version: 1, name: "app", providers: {} }), -); + join(config, 'gkit/profiles/app.json'), + JSON.stringify({ version: 1, name: 'app', providers: {} }), +) await writeFile( - join(profile, "site.json"), - JSON.stringify({ - version: 1, - site: "example.com", - origins: ["https://example.com"], - seeds: ["https://example.com/", "https://example.com/fr"], - delayMs: 0, - github: { repository: "owner/site" }, - }), -); -const bin = join(root, "bin"); -await mkdir(bin); + join(profile, 'site.json'), + JSON.stringify({ + version: 1, + site: 'example.com', + origins: ['https://example.com'], + seeds: ['https://example.com/', 'https://example.com/fr'], + delayMs: 0, + github: { repository: 'owner/site' }, + }), +) +const bin = join(root, 'bin') +await mkdir(bin) await writeFile( - join(bin, "grok"), - `#!/usr/bin/env bun + join(bin, 'grok'), + `#!/usr/bin/env bun import {appendFileSync} from "node:fs"; if(process.argv.includes("--version")){console.log("fixture-grok-1");process.exit(0)} const i=process.argv.indexOf("--tools");if(i<0||process.argv[i+1]!=="")process.exit(2); appendFileSync(process.env.SITE_TEST_ROOT+"/grok-calls","call\\n"); console.log(JSON.stringify({summary:"The title changed.",evidence:[{page_url:"https://example.com/",claim:"The snapshot title changed.",code_paths:["app/pages/index.vue"],confidence:"medium",caveat:"Runtime inputs may also affect output."}],unknowns:[]})); `, - { mode: 0o700 }, -); + { mode: 0o700 }, +) const env = { - ...process.env, - PATH: `${bin}:${process.env.PATH}`, - XDG_CONFIG_HOME: config, - XDG_STATE_HOME: join(root, "unused-state"), - SITE_TEST_ROOT: root, -}; + ...process.env, + PATH: `${bin}:${process.env.PATH}`, + XDG_CONFIG_HOME: config, + XDG_STATE_HOME: join(root, 'unused-state'), + SITE_TEST_ROOT: root, +} const run = async (args: string[], expected = 0) => { - const child = Bun.spawn([Bun.which("bun")!, cli, ...args], { - env, - stdout: "pipe", - stderr: "pipe", - }); - const [out, err, code] = await Promise.all([ - new Response(child.stdout).text(), - new Response(child.stderr).text(), - child.exited, - ]); - assert.equal(code, expected, out + err); - return JSON.parse(out); -}; + const child = Bun.spawn([Bun.which('bun')!, cli, ...args], { + env, + stdout: 'pipe', + stderr: 'pipe', + }) + const [out, err, code] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]) + assert.equal(code, expected, out + err) + return JSON.parse(out) +} const build = async (name: string, title: string) => { - const path = join(root, name); - await mkdir(join(path, "public"), { recursive: true }); - await mkdir(join(path, "server")); - const html = `${title}

Example

`; - await writeFile(join(path, "public/index.html"), html); - await writeFile( - join(path, "server/index.mjs"), - `import http from "node:http";const server=http.createServer((req,res)=>{res.setHeader("content-type","text/html");res.end(${JSON.stringify(html.replace('href="https://example.com/"', 'href="https://example.com/fr"'))})});server.listen(Number(process.env.PORT),"127.0.0.1");process.on("SIGTERM",()=>server.close());`, - ); - return path; -}; -let scenarios = 0; + const path = join(root, name) + await mkdir(join(path, 'public'), { recursive: true }) + await mkdir(join(path, 'server')) + const html = `${title}

Example

` + await writeFile(join(path, 'public/index.html'), html) + await writeFile( + join(path, 'server/index.mjs'), + `import http from "node:http";const server=http.createServer((req,res)=>{res.setHeader("content-type","text/html");res.end(${JSON.stringify(html.replace('href="https://example.com/"', 'href="https://example.com/fr"'))})});server.listen(Number(process.env.PORT),"127.0.0.1");process.on("SIGTERM",()=>server.close());`, + ) + return path +} +let scenarios = 0 try { - await git(["init", "-b", "main"]); - await git(["config", "user.email", "fixture@example.invalid"]); - await git(["config", "user.name", "Fixture"]); - await mkdir(join(repo, "app/pages"), { recursive: true }); - await writeFile(join(repo, "app/pages/index.vue"), "\n"); - await git(["add", "."]); - await git(["commit", "-m", "A"]); - const a = await git(["rev-parse", "HEAD"]); - await writeFile(join(repo, "app/pages/index.vue"), "\n"); - await git(["commit", "-am", "B"]); - const b = await git(["rev-parse", "HEAD"]); - const buildA = await build("output-a", "A"); - const buildB = await build("output-b", "B"); - const snapA = join(root, "a"); - const snapB = join(root, "b"); - await run([ - "--profile", - "app", - "site", - "snapshot", - "--build", - buildA, - "--revision", - a, - "--repo", - repo, - "--context", - "fixture", - "--out", - snapA, - "--dry-run", - ]); - await assert.rejects(readFile(join(snapA, "manifest.json"))); - scenarios++; - for (const [output, sha, out] of [ - [buildA, a, snapA], - [buildB, b, snapB], - ]) { - const result = await run([ - "--profile", - "app", - "site", - "snapshot", - "--build", - output!, - "--revision", - sha!, - "--repo", - repo, - "--context", - "fixture", - "--out", - out!, - ]); - assert.equal(result.data.coverage.observed, 2); - } - const captured = await readSnapshot(snapA); - assert.deepEqual( - captured.pages.map((page) => page.mode), - ["prerender", "server-http"], - ); - assert.equal(captured.pages[0]?.http_status, null); - scenarios++; - const first = await readFile(join(snapA, "manifest.json"), "utf8"); - await run( - [ - "--profile", - "app", - "site", - "snapshot", - "--build", - buildB, - "--revision", - b, - "--repo", - repo, - "--context", - "fixture", - "--out", - snapA, - ], - 1, - ); - assert.equal(await readFile(join(snapA, "manifest.json"), "utf8"), first); - scenarios++; - const moved = join(root, "moved"); - await cp(snapA, moved, { recursive: true }); - await rm(snapA, { recursive: true }); - assert.equal((await readSnapshot(moved)).manifest.snapshot_id, captured.manifest.snapshot_id); - scenarios++; - const diff = await run(["site", "diff", moved, snapB]); - assert.equal(diff.data.summary.changed_pages, 2); - assert.equal(diff.data.coverage.comparable_urls, 2); - scenarios++; - await rm(profile, { recursive: true }); - const audited = await run(["site", "audit", moved]); - assert.equal(audited.ok, true); - scenarios++; - await mkdir(profile); - await writeFile( - join(profile, "site.json"), - JSON.stringify({ - version: 1, - site: "example.com", - origins: ["https://example.com"], - seeds: ["https://example.com/"], - github: { repository: "owner/site" }, - }), - ); - const input = join(root, "input.json"); - await run([ - "--profile", - "app", - "site", - "explain", - moved, - snapB, - "--repo", - repo, - "--model", - "fixture-model", - "--out", - input, - "--dry-run", - ]); - const prepared = JSON.parse(await readFile(input, "utf8")); - assert.equal(prepared.input.code.method, "git-diff-two-trees"); - await assert.rejects(readFile(join(root, "grok-calls"))); - scenarios++; - for (const suffix of ["first", "cached"]) { - const result = await run([ - "--profile", - "app", - "site", - "explain", - moved, - snapB, - "--repo", - repo, - "--model", - "fixture-model", - "--out", - join(root, `${suffix}.json`), - ]); - assert.equal(result.data.status, suffix === "first" ? "generated" : "cached"); - } - assert.equal((await readFile(join(root, "grok-calls"), "utf8")).trim(), "call"); - scenarios++; - await run( - [ - "--profile", - "app", - "site", - "explain", - moved, - snapB, - "--repo", - repo, - "--model", - "fixture-model", - "--out", - join(root, "first.json"), - ], - 1, - ); - assert.equal((await readFile(join(root, "grok-calls"), "utf8")).trim(), "call"); - scenarios++; - await rm(join(profile, "cache"), { recursive: true }); - await run([ - "--profile", - "app", - "site", - "explain", - moved, - snapB, - "--repo", - repo, - "--model", - "fixture-model", - "--out", - join(root, "rebuilt.json"), - ]); - assert.equal((await readFile(join(root, "grok-calls"), "utf8")).trim().split("\n").length, 2); - scenarios++; - const cacheDirectory = join(profile, "cache/site-explanations"); - const cacheFiles = await readdir(cacheDirectory); - assert.equal(cacheFiles.length, 1); - await writeFile(join(cacheDirectory, cacheFiles[0]!), "damaged"); - for (const suffix of ["repaired", "repair-cached"]) { - const result = await run([ - "--profile", - "app", - "site", - "explain", - moved, - snapB, - "--repo", - repo, - "--model", - "fixture-model", - "--out", - join(root, `${suffix}.json`), - ]); - assert.equal(result.data.status, suffix === "repaired" ? "generated" : "cached"); - } - assert.equal((await readFile(join(root, "grok-calls"), "utf8")).trim().split("\n").length, 3); - scenarios++; - await rm(join(buildA, "public"), { recursive: true }); - await run([ - "--profile", - "app", - "site", - "snapshot", - "--build", - buildA, - "--revision", - a, - "--repo", - repo, - "--context", - "fixture", - "--out", - join(root, "ssr-only"), - ]); - const ssrOnly = await readSnapshot(join(root, "ssr-only")); - assert.equal(ssrOnly.pages[0]?.mode, "server-http"); - assert.equal(ssrOnly.manifest.coverage.observed, 1); - scenarios++; - const original = await readSnapshot(moved); - await writeFile(join(moved, "content", original.pages[0]!.html_hash!), "tampered"); - await run(["site", "audit", moved], 1); - scenarios++; - const incomplete = join(root, "incomplete"); - await mkdir(incomplete); - await writeFile(join(incomplete, "pages.jsonl"), ""); - await run(["site", "audit", incomplete], 1); - scenarios++; - await assert.rejects(readdir(join(root, "unused-state"))); - scenarios++; - console.log(JSON.stringify({ ok: true, scenarios })); + await git(['init', '-b', 'main']) + await git(['config', 'user.email', 'fixture@example.invalid']) + await git(['config', 'user.name', 'Fixture']) + await mkdir(join(repo, 'app/pages'), { recursive: true }) + await writeFile( + join(repo, 'app/pages/index.vue'), + '\n', + ) + await git(['add', '.']) + await git(['commit', '-m', 'A']) + const a = await git(['rev-parse', 'HEAD']) + await writeFile( + join(repo, 'app/pages/index.vue'), + '\n', + ) + await git(['commit', '-am', 'B']) + const b = await git(['rev-parse', 'HEAD']) + const buildA = await build('output-a', 'A') + const buildB = await build('output-b', 'B') + const snapA = join(root, 'a') + const snapB = join(root, 'b') + await run([ + '--profile', + 'app', + 'site', + 'snapshot', + '--build', + buildA, + '--revision', + a, + '--repo', + repo, + '--context', + 'fixture', + '--out', + snapA, + '--dry-run', + ]) + await assert.rejects(readFile(join(snapA, 'manifest.json'))) + scenarios++ + for (const [output, sha, out] of [ + [buildA, a, snapA], + [buildB, b, snapB], + ]) { + const result = await run([ + '--profile', + 'app', + 'site', + 'snapshot', + '--build', + output!, + '--revision', + sha!, + '--repo', + repo, + '--context', + 'fixture', + '--out', + out!, + ]) + assert.equal(result.data.coverage.observed, 2) + } + const captured = await readSnapshot(snapA) + assert.deepEqual( + captured.pages.map((page) => page.mode), + ['prerender', 'server-http'], + ) + assert.equal(captured.pages[0]?.http_status, null) + scenarios++ + const first = await readFile(join(snapA, 'manifest.json'), 'utf8') + await run( + [ + '--profile', + 'app', + 'site', + 'snapshot', + '--build', + buildB, + '--revision', + b, + '--repo', + repo, + '--context', + 'fixture', + '--out', + snapA, + ], + 1, + ) + assert.equal(await readFile(join(snapA, 'manifest.json'), 'utf8'), first) + scenarios++ + const moved = join(root, 'moved') + await cp(snapA, moved, { recursive: true }) + await rm(snapA, { recursive: true }) + assert.equal( + (await readSnapshot(moved)).manifest.snapshot_id, + captured.manifest.snapshot_id, + ) + scenarios++ + const diff = await run(['site', 'diff', moved, snapB]) + assert.equal(diff.data.summary.changed_pages, 2) + assert.equal(diff.data.coverage.comparable_urls, 2) + scenarios++ + await rm(profile, { recursive: true }) + const audited = await run(['site', 'audit', moved]) + assert.equal(audited.ok, true) + scenarios++ + await mkdir(profile) + await writeFile( + join(profile, 'site.json'), + JSON.stringify({ + version: 1, + site: 'example.com', + origins: ['https://example.com'], + seeds: ['https://example.com/'], + github: { repository: 'owner/site' }, + }), + ) + const input = join(root, 'input.json') + await run([ + '--profile', + 'app', + 'site', + 'explain', + moved, + snapB, + '--repo', + repo, + '--model', + 'fixture-model', + '--out', + input, + '--dry-run', + ]) + const prepared = JSON.parse(await readFile(input, 'utf8')) + assert.equal(prepared.input.code.method, 'git-diff-two-trees') + await assert.rejects(readFile(join(root, 'grok-calls'))) + scenarios++ + for (const suffix of ['first', 'cached']) { + const result = await run([ + '--profile', + 'app', + 'site', + 'explain', + moved, + snapB, + '--repo', + repo, + '--model', + 'fixture-model', + '--out', + join(root, `${suffix}.json`), + ]) + assert.equal( + result.data.status, + suffix === 'first' ? 'generated' : 'cached', + ) + } + assert.equal( + (await readFile(join(root, 'grok-calls'), 'utf8')).trim(), + 'call', + ) + scenarios++ + await run( + [ + '--profile', + 'app', + 'site', + 'explain', + moved, + snapB, + '--repo', + repo, + '--model', + 'fixture-model', + '--out', + join(root, 'first.json'), + ], + 1, + ) + assert.equal( + (await readFile(join(root, 'grok-calls'), 'utf8')).trim(), + 'call', + ) + scenarios++ + await rm(join(profile, 'cache'), { recursive: true }) + await run([ + '--profile', + 'app', + 'site', + 'explain', + moved, + snapB, + '--repo', + repo, + '--model', + 'fixture-model', + '--out', + join(root, 'rebuilt.json'), + ]) + assert.equal( + (await readFile(join(root, 'grok-calls'), 'utf8')).trim().split('\n') + .length, + 2, + ) + scenarios++ + const cacheDirectory = join(profile, 'cache/site-explanations') + const cacheFiles = await readdir(cacheDirectory) + assert.equal(cacheFiles.length, 1) + await writeFile(join(cacheDirectory, cacheFiles[0]!), 'damaged') + for (const suffix of ['repaired', 'repair-cached']) { + const result = await run([ + '--profile', + 'app', + 'site', + 'explain', + moved, + snapB, + '--repo', + repo, + '--model', + 'fixture-model', + '--out', + join(root, `${suffix}.json`), + ]) + assert.equal( + result.data.status, + suffix === 'repaired' ? 'generated' : 'cached', + ) + } + assert.equal( + (await readFile(join(root, 'grok-calls'), 'utf8')).trim().split('\n') + .length, + 3, + ) + scenarios++ + await rm(join(buildA, 'public'), { recursive: true }) + await run([ + '--profile', + 'app', + 'site', + 'snapshot', + '--build', + buildA, + '--revision', + a, + '--repo', + repo, + '--context', + 'fixture', + '--out', + join(root, 'ssr-only'), + ]) + const ssrOnly = await readSnapshot(join(root, 'ssr-only')) + assert.equal(ssrOnly.pages[0]?.mode, 'server-http') + assert.equal(ssrOnly.manifest.coverage.observed, 1) + scenarios++ + const original = await readSnapshot(moved) + await writeFile( + join(moved, 'content', original.pages[0]!.html_hash!), + 'tampered', + ) + await run(['site', 'audit', moved], 1) + scenarios++ + const incomplete = join(root, 'incomplete') + await mkdir(incomplete) + await writeFile(join(incomplete, 'pages.jsonl'), '') + await run(['site', 'audit', incomplete], 1) + scenarios++ + await assert.rejects(readdir(join(root, 'unused-state'))) + scenarios++ + console.log(JSON.stringify({ ok: true, scenarios })) } finally { - await rm(root, { recursive: true, force: true }); + await rm(root, { recursive: true, force: true }) } diff --git a/packages/gkit/scripts/verify-package-artifact.ts b/packages/gkit/scripts/verify-package-artifact.ts index 3aa0f62..f9511cb 100644 --- a/packages/gkit/scripts/verify-package-artifact.ts +++ b/packages/gkit/scripts/verify-package-artifact.ts @@ -1,257 +1,312 @@ -import { constants } from "node:fs"; -import { chmod, copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { createHash } from "node:crypto"; -import { tmpdir } from "node:os"; -import { basename, join, resolve } from "node:path"; +import { createHash } from 'node:crypto' +import { constants } from 'node:fs' +import { + chmod, + copyFile, + mkdir, + mkdtemp, + readFile, + rm, + writeFile, +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { basename, join, resolve } from 'node:path' type PackResult = Readonly<{ - filename: string; - files?: readonly Readonly<{ path: string }>[]; -}>; + filename: string + files?: readonly Readonly<{ path: string }>[] +}> type PackageJson = Readonly<{ - name?: unknown; - version?: unknown; - private?: unknown; -}>; + name?: unknown + version?: unknown + private?: unknown +}> -const allowedTopLevelEntries = new Set(["dist", "docs", "generated", "package.json"]); +const allowedTopLevelEntries = new Set([ + 'dist', + 'docs', + 'generated', + 'package.json', +]) const forbiddenPathPattern = - /(^|\/)(?:\.env(?:\..*)?|\.npmrc|bunfig\.toml|[^/]*\.test\.[^/]+|__tests__|evals?|scripts?|sources?|policy)(?:\/|$)/i; + /(^|\/)(?:\.env(?:\..*)?|\.npmrc|bunfig\.toml|[^/]*\.test\.[^/]+|__tests__|evals?|scripts?|sources?|policy)(?:\/|$)/i const secretMaterialPatterns = [ - /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/, - /\bgh(?:p|o|u|s|r)_[A-Za-z0-9]{20,}\b/, - /\bgithub_pat_[A-Za-z0-9_]{20,}\b/, -] as const; + /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/, + /\bgh(?:p|o|u|s|r)_[A-Za-z0-9]{20,}\b/, + /\bgithub_pat_[A-Za-z0-9_]{20,}\b/, +] as const -const packageRoot = resolve(import.meta.dir, ".."); -const outputDirectory = parseOutputDirectory(Bun.argv.slice(2)); -const temporaryRoot = await mkdtemp(join(tmpdir(), "gkit-package-artifact-")); -const packDirectory = join(temporaryRoot, "pack"); -const consumerDirectory = join(temporaryRoot, "consumer"); +const packageRoot = resolve(import.meta.dir, '..') +const outputDirectory = parseOutputDirectory(Bun.argv.slice(2)) +const temporaryRoot = await mkdtemp(join(tmpdir(), 'gkit-package-artifact-')) +const packDirectory = join(temporaryRoot, 'pack') +const consumerDirectory = join(temporaryRoot, 'consumer') try { - await Promise.all([ - mkdir(packDirectory, { recursive: true }), - mkdir(consumerDirectory, { recursive: true }), - ]); - const packageJson = JSON.parse( - await readFile(join(packageRoot, "package.json"), "utf8"), - ) as PackageJson; - if (packageJson.name !== "gkit" || typeof packageJson.version !== "string") { - throw new Error("The package must have name gkit and a string version."); - } - if (packageJson.private !== true) { - throw new Error("The gkit package must remain private to prevent registry publication."); - } + await Promise.all([ + mkdir(packDirectory, { recursive: true }), + mkdir(consumerDirectory, { recursive: true }), + ]) + const packageJson = JSON.parse( + await readFile(join(packageRoot, 'package.json'), 'utf8'), + ) as PackageJson + if (packageJson.name !== 'gkit' || typeof packageJson.version !== 'string') { + throw new Error('The package must have name gkit and a string version.') + } + if (packageJson.private !== true) { + throw new Error( + 'The gkit package must remain private to prevent registry publication.', + ) + } - // The artifact ships a Bun bundle: runtime deps (incl. the private @celados/argc) - // are inlined, so public consumers install with no registry auth. - await runCommand(["bun", "run", "build"], packageRoot); - const packed = await runCommand( - ["npm", "pack", "--json", "--pack-destination", packDirectory, packageRoot], - temporaryRoot, - ); - const packResults = JSON.parse(packed.stdout) as PackResult[]; - const packResult = packResults[0]; - if (!packResult?.filename || !packResult.files) { - throw new Error("npm pack did not report the package filename and contents."); - } - await verifyPackageContents(packResult.files.map((file) => file.path)); + // The artifact ships a Bun bundle: runtime deps (incl. the private @celados/argc) + // are inlined, so public consumers install with no registry auth. + await runCommand(['bun', 'run', 'build'], packageRoot) + const packed = await runCommand( + ['npm', 'pack', '--json', '--pack-destination', packDirectory, packageRoot], + temporaryRoot, + ) + const packResults = JSON.parse(packed.stdout) as PackResult[] + const packResult = packResults[0] + if (!packResult?.filename || !packResult.files) { + throw new Error( + 'npm pack did not report the package filename and contents.', + ) + } + await verifyPackageContents(packResult.files.map((file) => file.path)) - const tarballPath = join(packDirectory, packResult.filename); - const globalDirectory = join(consumerDirectory, "global"); - const globalBinDirectory = join(consumerDirectory, "bin"); - await runCommand( - [ - "bun", - "add", - "--global", - "--registry", - "https://registry.npmjs.org", - "--network-concurrency", - "8", - `gkit@${tarballPath}`, - ], - consumerDirectory, - { - BUN_INSTALL_GLOBAL_DIR: globalDirectory, - BUN_INSTALL_BIN: globalBinDirectory, - BUN_INSTALL_CACHE_DIR: join(consumerDirectory, "cache"), - }, - ); + const tarballPath = join(packDirectory, packResult.filename) + const globalDirectory = join(consumerDirectory, 'global') + const globalBinDirectory = join(consumerDirectory, 'bin') + await runCommand( + [ + 'bun', + 'add', + '--global', + '--registry', + 'https://registry.npmjs.org', + '--network-concurrency', + '8', + `gkit@${tarballPath}`, + ], + consumerDirectory, + { + BUN_INSTALL_GLOBAL_DIR: globalDirectory, + BUN_INSTALL_BIN: globalBinDirectory, + BUN_INSTALL_CACHE_DIR: join(consumerDirectory, 'cache'), + }, + ) - const installedPackageJson = await readFile( - join(globalDirectory, "node_modules", "gkit", "package.json"), - "utf8", - ); - if (installedPackageJson.includes('"catalog:"')) { - throw new Error("The installed gkit artifact still contains workspace-only catalog ranges."); - } + const installedPackageJson = await readFile( + join(globalDirectory, 'node_modules', 'gkit', 'package.json'), + 'utf8', + ) + if (installedPackageJson.includes('"catalog:"')) { + throw new Error( + 'The installed gkit artifact still contains workspace-only catalog ranges.', + ) + } - const gkit = join(globalBinDirectory, "gkit"); - await runCommand([gkit, "--schema", "site"], consumerDirectory); - const isolatedConfig = join(consumerDirectory, "config"); - const siteProfile = join(isolatedConfig, "gkit", "profiles", "package-test"); - await mkdir(siteProfile, { recursive: true }); - await writeFile( - join(siteProfile, "..", "package-test.json"), - JSON.stringify({ version: 1, name: "package-test", providers: {} }), - ); - await writeFile( - join(siteProfile, "site.json"), - JSON.stringify({ - version: 1, - site: "example.test", - origins: ["https://example.test"], - seeds: ["https://example.test/"], - github: { repository: "fixture/site" }, - }), - ); - const discovery = await runCommand( - [ - gkit, - "--profile", - "package-test", - "site", - "discover", - "--dry-run", - "--out", - join(consumerDirectory, "discovery"), - ], - consumerDirectory, - { XDG_CONFIG_HOME: isolatedConfig }, - ); - if (JSON.parse(discovery.stdout).data?.status !== "prepared") { - throw new Error("The installed artifact could not load the site discovery runtime."); - } - await runCommand([gkit, "--schema", "gsc"], consumerDirectory); - const described = await runCommand( - [gkit, "describe", "--id", "gsc.properties.list"], - consumerDirectory, - ); - const capability = JSON.parse(described.stdout) as { id?: unknown }; - if (capability.id !== "gsc.properties.list") { - throw new Error("The installed gkit artifact returned an unexpected capability."); - } - const docs = await runCommand([gkit, "docs", "--provider", "gsc"], consumerDirectory); - const capabilities = await readFile(join(docs.stdout.trim(), "capabilities.md"), "utf8"); - if (!capabilities.includes("gsc.properties.list")) { - throw new Error("The installed gkit artifact could not read its provider documentation."); - } + const gkit = join(globalBinDirectory, 'gkit') + await runCommand([gkit, '--schema', 'site'], consumerDirectory) + const isolatedConfig = join(consumerDirectory, 'config') + const siteProfile = join(isolatedConfig, 'gkit', 'profiles', 'package-test') + await mkdir(siteProfile, { recursive: true }) + await writeFile( + join(siteProfile, '..', 'package-test.json'), + JSON.stringify({ version: 1, name: 'package-test', providers: {} }), + ) + await writeFile( + join(siteProfile, 'site.json'), + JSON.stringify({ + version: 1, + site: 'example.test', + origins: ['https://example.test'], + seeds: ['https://example.test/'], + github: { repository: 'fixture/site' }, + }), + ) + const discovery = await runCommand( + [ + gkit, + '--profile', + 'package-test', + 'site', + 'discover', + '--dry-run', + '--out', + join(consumerDirectory, 'discovery'), + ], + consumerDirectory, + { XDG_CONFIG_HOME: isolatedConfig }, + ) + if (JSON.parse(discovery.stdout).data?.status !== 'prepared') { + throw new Error( + 'The installed artifact could not load the site discovery runtime.', + ) + } + await runCommand([gkit, '--schema', 'gsc'], consumerDirectory) + const described = await runCommand( + [gkit, 'describe', '--id', 'gsc.properties.list'], + consumerDirectory, + ) + const capability = JSON.parse(described.stdout) as { id?: unknown } + if (capability.id !== 'gsc.properties.list') { + throw new Error( + 'The installed gkit artifact returned an unexpected capability.', + ) + } + const docs = await runCommand( + [gkit, 'docs', '--provider', 'gsc'], + consumerDirectory, + ) + const capabilities = await readFile( + join(docs.stdout.trim(), 'capabilities.md'), + 'utf8', + ) + if (!capabilities.includes('gsc.properties.list')) { + throw new Error( + 'The installed gkit artifact could not read its provider documentation.', + ) + } - // The bare bundle is the team-machine install: it must run with nothing next to it. - const bundleDirectory = join(consumerDirectory, "standalone"); - await mkdir(bundleDirectory, { recursive: true }); - const bundlePath = join(bundleDirectory, "gkit"); - await copyFile(join(packageRoot, "dist", "gkit.js"), bundlePath); - await chmod(bundlePath, 0o755); - const standaloneEnvironment = { XDG_CACHE_HOME: join(consumerDirectory, "standalone-cache") }; - await runCommand([bundlePath, "--schema", "gsc"], bundleDirectory, standaloneEnvironment); - const standaloneDocs = await runCommand( - [bundlePath, "docs", "--provider", "gsc"], - bundleDirectory, - standaloneEnvironment, - ); - const standaloneCapabilities = await readFile( - join(standaloneDocs.stdout.trim(), "capabilities.md"), - "utf8", - ); - if (!standaloneCapabilities.includes("gsc.properties.list")) { - throw new Error("The standalone gkit bundle could not read its embedded documentation."); - } + // The bare bundle is the team-machine install: it must run with nothing next to it. + const bundleDirectory = join(consumerDirectory, 'standalone') + await mkdir(bundleDirectory, { recursive: true }) + const bundlePath = join(bundleDirectory, 'gkit') + await copyFile(join(packageRoot, 'dist', 'gkit.js'), bundlePath) + await chmod(bundlePath, 0o755) + const standaloneEnvironment = { + XDG_CACHE_HOME: join(consumerDirectory, 'standalone-cache'), + } + await runCommand( + [bundlePath, '--schema', 'gsc'], + bundleDirectory, + standaloneEnvironment, + ) + const standaloneDocs = await runCommand( + [bundlePath, 'docs', '--provider', 'gsc'], + bundleDirectory, + standaloneEnvironment, + ) + const standaloneCapabilities = await readFile( + join(standaloneDocs.stdout.trim(), 'capabilities.md'), + 'utf8', + ) + if (!standaloneCapabilities.includes('gsc.properties.list')) { + throw new Error( + 'The standalone gkit bundle could not read its embedded documentation.', + ) + } - if (outputDirectory) { - await writeReleaseAssets(tarballPath, packageJson.version, outputDirectory); - } - process.stdout.write(`Verified installable gkit artifact: ${packResult.filename}\n`); + if (outputDirectory) { + await writeReleaseAssets(tarballPath, packageJson.version, outputDirectory) + } + process.stdout.write( + `Verified installable gkit artifact: ${packResult.filename}\n`, + ) } finally { - await rm(temporaryRoot, { recursive: true, force: true }); + await rm(temporaryRoot, { recursive: true, force: true }) } -function parseOutputDirectory(arguments_: readonly string[]): string | undefined { - if (arguments_.length === 0) return undefined; - if (arguments_.length !== 2 || arguments_[0] !== "--output-dir" || !arguments_[1]) { - throw new Error("Usage: verify-package-artifact.ts [--output-dir ]"); - } - return resolve(arguments_[1]); +function parseOutputDirectory( + arguments_: readonly string[], +): string | undefined { + if (arguments_.length === 0) return undefined + if ( + arguments_.length !== 2 || + arguments_[0] !== '--output-dir' || + !arguments_[1] + ) { + throw new Error( + 'Usage: verify-package-artifact.ts [--output-dir ]', + ) + } + return resolve(arguments_[1]) } async function verifyPackageContents(paths: readonly string[]): Promise { - if (!paths.includes("dist/gkit.js") || !paths.includes("package.json")) { - throw new Error("The package is missing its binary or package manifest."); - } - if (!paths.some((path) => path.startsWith("generated/"))) { - throw new Error("The package is missing generated provider manifests."); - } - if (!paths.some((path) => path.startsWith("docs/"))) { - throw new Error("The package is missing provider documentation."); - } - for (const path of paths) { - const topLevelEntry = path.split("/", 1)[0]; - if (!topLevelEntry || !allowedTopLevelEntries.has(topLevelEntry)) { - throw new Error(`Unexpected package entry: ${path}`); - } - if (forbiddenPathPattern.test(path)) { - throw new Error(`Forbidden package entry: ${path}`); - } - const content = await readFile(join(packageRoot, path), "utf8"); - if (secretMaterialPatterns.some((pattern) => pattern.test(content))) { - throw new Error(`Potential secret material found in package entry: ${path}`); - } - } + if (!paths.includes('dist/gkit.js') || !paths.includes('package.json')) { + throw new Error('The package is missing its binary or package manifest.') + } + if (!paths.some((path) => path.startsWith('generated/'))) { + throw new Error('The package is missing generated provider manifests.') + } + if (!paths.some((path) => path.startsWith('docs/'))) { + throw new Error('The package is missing provider documentation.') + } + for (const path of paths) { + const topLevelEntry = path.split('/', 1)[0] + if (!topLevelEntry || !allowedTopLevelEntries.has(topLevelEntry)) { + throw new Error(`Unexpected package entry: ${path}`) + } + if (forbiddenPathPattern.test(path)) { + throw new Error(`Forbidden package entry: ${path}`) + } + const content = await readFile(join(packageRoot, path), 'utf8') + if (secretMaterialPatterns.some((pattern) => pattern.test(content))) { + throw new Error( + `Potential secret material found in package entry: ${path}`, + ) + } + } } async function writeReleaseAssets( - tarballPath: string, - version: string, - destination: string, + tarballPath: string, + version: string, + destination: string, ): Promise { - await mkdir(destination, { recursive: true }); - const exactName = `gkit-${version}.tgz`; - const stableName = "gkit.tgz"; - const exactPath = join(destination, exactName); - const stablePath = join(destination, stableName); - await copyFile(tarballPath, exactPath, constants.COPYFILE_EXCL); - await copyFile(tarballPath, stablePath, constants.COPYFILE_EXCL); - const bundleName = "gkit"; - const bundlePath = join(destination, bundleName); - await copyFile(join(packageRoot, "dist", "gkit.js"), bundlePath, constants.COPYFILE_EXCL); - await chmod(bundlePath, 0o755); - const digest = createHash("sha256") - .update(await readFile(tarballPath)) - .digest("hex"); - const bundleDigest = createHash("sha256") - .update(await readFile(bundlePath)) - .digest("hex"); - await writeFile( - join(destination, "SHA256SUMS"), - `${digest} ${exactName}\n${digest} ${stableName}\n${bundleDigest} ${bundleName}\n`, - { encoding: "utf8", flag: "wx" }, - ); + await mkdir(destination, { recursive: true }) + const exactName = `gkit-${version}.tgz` + const stableName = 'gkit.tgz' + const exactPath = join(destination, exactName) + const stablePath = join(destination, stableName) + await copyFile(tarballPath, exactPath, constants.COPYFILE_EXCL) + await copyFile(tarballPath, stablePath, constants.COPYFILE_EXCL) + const bundleName = 'gkit' + const bundlePath = join(destination, bundleName) + await copyFile( + join(packageRoot, 'dist', 'gkit.js'), + bundlePath, + constants.COPYFILE_EXCL, + ) + await chmod(bundlePath, 0o755) + const digest = createHash('sha256') + .update(await readFile(tarballPath)) + .digest('hex') + const bundleDigest = createHash('sha256') + .update(await readFile(bundlePath)) + .digest('hex') + await writeFile( + join(destination, 'SHA256SUMS'), + `${digest} ${exactName}\n${digest} ${stableName}\n${bundleDigest} ${bundleName}\n`, + { encoding: 'utf8', flag: 'wx' }, + ) } async function runCommand( - command: readonly string[], - cwd: string, - additionalEnvironment: Readonly> = {}, + command: readonly string[], + cwd: string, + additionalEnvironment: Readonly> = {}, ): Promise<{ stdout: string; stderr: string }> { - const child = Bun.spawn([...command], { - cwd, - env: { ...Bun.env, ...additionalEnvironment, CI: "1" }, - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - }); - const [exitCode, stdout, stderr] = await Promise.all([ - child.exited, - new Response(child.stdout).text(), - new Response(child.stderr).text(), - ]); - if (exitCode !== 0) { - throw new Error( - `Command failed (${exitCode}): ${command.map((value) => basename(value)).join(" ")}\n${stdout}${stderr}`.trimEnd(), - ); - } - return { stdout, stderr }; + const child = Bun.spawn([...command], { + cwd, + env: { ...Bun.env, ...additionalEnvironment, CI: '1' }, + stdin: 'ignore', + stdout: 'pipe', + stderr: 'pipe', + }) + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]) + if (exitCode !== 0) { + throw new Error( + `Command failed (${exitCode}): ${command.map((value) => basename(value)).join(' ')}\n${stdout}${stderr}`.trimEnd(), + ) + } + return { stdout, stderr } } diff --git a/packages/gkit/src/args.test.ts b/packages/gkit/src/args.test.ts index d44892d..d0c0dad 100644 --- a/packages/gkit/src/args.test.ts +++ b/packages/gkit/src/args.test.ts @@ -1,244 +1,292 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest' -import { parseArgs } from "./args"; +import { parseArgs } from './args' -describe("gkit argv parser", () => { - it("accepts the public spaced command and global profile", () => { - expect( - parseArgs([ - "--profile", - "app-a", - "dataforseo", - "api", - "call", - "--capability-id", - "dataforseo.backlinks.bulk_ranks.live", - "--input", - "@req.json", - "--allow-spend", - "--max-spend-usd", - "0.05", - "--out", - "r.json", - ]), - ).toEqual({ - kind: "dataforseo-call", - profileFlag: "app-a", - capabilityId: "dataforseo.backlinks.bulk_ranks.live", - input: "@req.json", - allowSpend: true, - maxSpendUsd: "0.05", - out: "r.json", - force: false, - dryRun: false, - }); - }); +describe('gkit argv parser', () => { + it('accepts the public spaced command and global profile', () => { + expect( + parseArgs([ + '--profile', + 'app-a', + 'dataforseo', + 'api', + 'call', + '--capability-id', + 'dataforseo.backlinks.bulk_ranks.live', + '--input', + '@req.json', + '--allow-spend', + '--max-spend-usd', + '0.05', + '--out', + 'r.json', + ]), + ).toEqual({ + kind: 'dataforseo-call', + profileFlag: 'app-a', + capabilityId: 'dataforseo.backlinks.bulk_ranks.live', + input: '@req.json', + allowSpend: true, + maxSpendUsd: '0.05', + out: 'r.json', + force: false, + dryRun: false, + }) + }) - it("keeps discovery commands profile-free", () => { - expect(parseArgs(["--schema"])).toEqual({ kind: "schema", selector: null }); - expect(parseArgs(["operations", "--limit", "5"])).toEqual({ kind: "operations-list", limit: 5 }); - expect(parseArgs(["--version"])).toEqual({ kind: "version" }); - expect(parseArgs(["@skill"])).toEqual({ kind: "skill", path: null }); - expect(parseArgs(["@skill", "SKILL.md"])).toEqual({ kind: "skill", path: "SKILL.md" }); - expect(parseArgs(["describe", "--id", "capability"])).toEqual({ - kind: "describe", - id: "capability", - }); - expect(() => parseArgs(["--profile", "app-a", "--schema"])).toThrow( - "--schema does not load a profile", - ); - expect(() => parseArgs(["--profile", "app-a", "@skill"])).toThrow( - "@skill does not load a profile", - ); - expect(parseArgs(["operations"])).toEqual({ kind: "operations-list", limit: 20 }); - expect(parseArgs(["operations", "list", "--limit", "5"])).toEqual({ - kind: "operations-list", - limit: 5, - }); - expect(parseArgs(["operations", "show", "--id", "op-1"])).toEqual({ - kind: "operations-show", - operationId: "op-1", - }); - }); + it('keeps discovery commands profile-free', () => { + expect(parseArgs(['--schema'])).toEqual({ kind: 'schema', selector: null }) + expect(parseArgs(['operations', '--limit', '5'])).toEqual({ + kind: 'operations-list', + limit: 5, + }) + expect(parseArgs(['--version'])).toEqual({ kind: 'version' }) + expect(parseArgs(['@skill'])).toEqual({ kind: 'skill', path: null }) + expect(parseArgs(['@skill', 'SKILL.md'])).toEqual({ + kind: 'skill', + path: 'SKILL.md', + }) + expect(parseArgs(['describe', '--id', 'capability'])).toEqual({ + kind: 'describe', + id: 'capability', + }) + expect(() => parseArgs(['--profile', 'app-a', '--schema'])).toThrow( + '--schema does not load a profile', + ) + expect(() => parseArgs(['--profile', 'app-a', '@skill'])).toThrow( + '@skill does not load a profile', + ) + expect(parseArgs(['operations'])).toEqual({ + kind: 'operations-list', + limit: 20, + }) + expect(parseArgs(['operations', 'list', '--limit', '5'])).toEqual({ + kind: 'operations-list', + limit: 5, + }) + expect(parseArgs(['operations', 'show', '--id', 'op-1'])).toEqual({ + kind: 'operations-show', + operationId: 'op-1', + }) + }) - it("accepts the read-only PostHog command without spend flags", () => { - expect( - parseArgs([ - "--profile", - "app-a", - "posthog", - "api", - "call", - "--capability-id", - "posthog.query.run", - "--input", - "@req.json", - "--out", - "result.json", - "--dry-run", - ]), - ).toEqual({ - kind: "posthog-call", - profileFlag: "app-a", - capabilityId: "posthog.query.run", - input: "@req.json", - out: "result.json", - force: false, - dryRun: true, - }); - expect(() => - parseArgs([ - "posthog", - "api", - "call", - "--capability-id", - "posthog.query.run", - "--input", - "@req.json", - "--allow-spend", - ]), - ).toThrow("Unknown flag"); - }); + it('accepts the read-only PostHog command without spend flags', () => { + expect( + parseArgs([ + '--profile', + 'app-a', + 'posthog', + 'api', + 'call', + '--capability-id', + 'posthog.query.run', + '--input', + '@req.json', + '--out', + 'result.json', + '--dry-run', + ]), + ).toEqual({ + kind: 'posthog-call', + profileFlag: 'app-a', + capabilityId: 'posthog.query.run', + input: '@req.json', + out: 'result.json', + force: false, + dryRun: true, + }) + expect(() => + parseArgs([ + 'posthog', + 'api', + 'call', + '--capability-id', + 'posthog.query.run', + '--input', + '@req.json', + '--allow-spend', + ]), + ).toThrow('Unknown flag') + }) - it("accepts the reviewed Google Ads call without spend or manager flags", () => { - expect( - parseArgs([ - "--profile", - "openclaw-web", - "google-ads", - "api", - "call", - "--capability-id", - "google-ads.query.gaql", - "--input", - "@gaql.json", - "--out", - "pages.json", - "--dry-run", - ]), - ).toEqual({ - kind: "google-ads-call", - profileFlag: "openclaw-web", - capabilityId: "google-ads.query.gaql", - input: "@gaql.json", - out: "pages.json", - force: false, - dryRun: true, - }); - expect(() => - parseArgs([ - "google-ads", - "api", - "call", - "--capability-id", - "google-ads.query.gaql", - "--input", - "{}", - "--login-customer-id", - "0987654321", - ]), - ).toThrow("Unknown flag"); - }); + it('accepts the reviewed Google Ads call without spend or manager flags', () => { + expect( + parseArgs([ + '--profile', + 'openclaw-web', + 'google-ads', + 'api', + 'call', + '--capability-id', + 'google-ads.query.gaql', + '--input', + '@gaql.json', + '--out', + 'pages.json', + '--dry-run', + ]), + ).toEqual({ + kind: 'google-ads-call', + profileFlag: 'openclaw-web', + capabilityId: 'google-ads.query.gaql', + input: '@gaql.json', + out: 'pages.json', + force: false, + dryRun: true, + }) + expect(() => + parseArgs([ + 'google-ads', + 'api', + 'call', + '--capability-id', + 'google-ads.query.gaql', + '--input', + '{}', + '--login-customer-id', + '0987654321', + ]), + ).toThrow('Unknown flag') + }) - it.each([ - ["bing", "bing.sites.list", "bing-call"], - ["gsc", "gsc.properties.list", "gsc-call"], - ["hubspot", "hubspot.crm.objects.list", "hubspot-call"], - ["pagespeed", "pagespeed.run", "pagespeed-call"], - ] as const)( - "accepts the read-only %s call without spend flags", - (provider, capabilityId, kind) => { - expect( - parseArgs([ - "--profile", - "app-a", - provider, - "api", - "call", - "--capability-id", - capabilityId, - "--input", - "{}", - "--out", - "result.json", - "--dry-run", - ]), - ).toEqual({ - kind, - profileFlag: "app-a", - capabilityId, - input: "{}", - ...(provider === "hubspot" || provider === "pagespeed" ? {} : { allowWrite: false }), - out: "result.json", - force: false, - dryRun: true, - }); - }, - ); + it.each([ + ['bing', 'bing.sites.list', 'bing-call'], + ['gsc', 'gsc.properties.list', 'gsc-call'], + ['hubspot', 'hubspot.crm.objects.list', 'hubspot-call'], + ['pagespeed', 'pagespeed.run', 'pagespeed-call'], + ] as const)( + 'accepts the read-only %s call without spend flags', + (provider, capabilityId, kind) => { + expect( + parseArgs([ + '--profile', + 'app-a', + provider, + 'api', + 'call', + '--capability-id', + capabilityId, + '--input', + '{}', + '--out', + 'result.json', + '--dry-run', + ]), + ).toEqual({ + kind, + profileFlag: 'app-a', + capabilityId, + input: '{}', + ...(provider === 'hubspot' || provider === 'pagespeed' + ? {} + : { allowWrite: false }), + out: 'result.json', + force: false, + dryRun: true, + }) + }, + ) - it("parses --allow-write for GSC and Bing calls so the effect gate receives it", () => { - expect( - parseArgs(["bing", "api", "call", "--capability-id", "bing.urls.submit", "--input", "{}", "--allow-write"]), - ).toMatchObject({ kind: "bing-call", allowWrite: true }); - expect( - parseArgs([ - "--profile", - "app-a", - "gsc", - "api", - "call", - "--capability-id", - "gsc.sitemaps.submit", - "--input", - "{}", - "--allow-write", - "--out", - "receipt.json", - ]), - ).toMatchObject({ kind: "gsc-call", capabilityId: "gsc.sitemaps.submit", allowWrite: true }); - expect(() => - parseArgs(["gsc", "api", "call", "--capability-id", "x", "--input", "{}", "--allow-write=yes"]), - ).toThrow("does not accept a value"); - }); + it('parses --allow-write for GSC and Bing calls so the effect gate receives it', () => { + expect( + parseArgs([ + 'bing', + 'api', + 'call', + '--capability-id', + 'bing.urls.submit', + '--input', + '{}', + '--allow-write', + ]), + ).toMatchObject({ kind: 'bing-call', allowWrite: true }) + expect( + parseArgs([ + '--profile', + 'app-a', + 'gsc', + 'api', + 'call', + '--capability-id', + 'gsc.sitemaps.submit', + '--input', + '{}', + '--allow-write', + '--out', + 'receipt.json', + ]), + ).toMatchObject({ + kind: 'gsc-call', + capabilityId: 'gsc.sitemaps.submit', + allowWrite: true, + }) + expect(() => + parseArgs([ + 'gsc', + 'api', + 'call', + '--capability-id', + 'x', + '--input', + '{}', + '--allow-write=yes', + ]), + ).toThrow('does not accept a value') + }) - it.each(["hubspot", "dataforseo"] as const)( - "rejects --allow-write for %s, which has no write capabilities", - (provider) => { - expect(() => - parseArgs([provider, "api", "call", "--capability-id", "x", "--input", "{}", "--allow-write"]), - ).toThrow("Unknown flag: --allow-write"); - }, - ); + it.each(['hubspot', 'dataforseo'] as const)( + 'rejects --allow-write for %s, which has no write capabilities', + (provider) => { + expect(() => + parseArgs([ + provider, + 'api', + 'call', + '--capability-id', + 'x', + '--input', + '{}', + '--allow-write', + ]), + ).toThrow('Unknown flag: --allow-write') + }, + ) - it("rejects unknown or duplicate flags before execution", () => { - expect(() => - parseArgs([ - "dataforseo", - "api", - "call", - "--capability-id", - "id", - "--capability-id", - "id-2", - "--input", - "@req.json", - ]), - ).toThrow("Duplicate flag"); - expect(() => - parseArgs([ - "dataforseo", - "api", - "call", - "--capability-id", - "id", - "--input", - "@req.json", - "--unknown", - "value", - ]), - ).toThrow("Unknown flag"); - expect(() => - parseArgs(["--profile", "app-a", "--profile=app-b", "dataforseo", "doctor"]), - ).toThrow("--profile may be provided only once"); - }); -}); + it('rejects unknown or duplicate flags before execution', () => { + expect(() => + parseArgs([ + 'dataforseo', + 'api', + 'call', + '--capability-id', + 'id', + '--capability-id', + 'id-2', + '--input', + '@req.json', + ]), + ).toThrow('Duplicate flag') + expect(() => + parseArgs([ + 'dataforseo', + 'api', + 'call', + '--capability-id', + 'id', + '--input', + '@req.json', + '--unknown', + 'value', + ]), + ).toThrow('Unknown flag') + expect(() => + parseArgs([ + '--profile', + 'app-a', + '--profile=app-b', + 'dataforseo', + 'doctor', + ]), + ).toThrow('--profile may be provided only once') + }) +}) diff --git a/packages/gkit/src/args.ts b/packages/gkit/src/args.ts index 76df5ac..47debf6 100644 --- a/packages/gkit/src/args.ts +++ b/packages/gkit/src/args.ts @@ -1,442 +1,475 @@ -import { parseSiteArgs, type SiteCommand } from "./site/commands"; -import { GkitFailure } from "./envelope"; +import { GkitFailure } from './envelope' +import { parseSiteArgs, type SiteCommand } from './site/commands' export type ParsedCommand = - | SiteCommand - | { kind: "help" } - | { kind: "version" } - | { kind: "schema"; selector: string | null } - | { kind: "skill"; path: string | null } - | { kind: "describe"; id: string } - | { kind: "docs"; provider: string | null } - | { kind: "operations-list"; limit: number } - | { kind: "operations-show"; operationId: string } - | { - kind: "operations-reconcile"; - operationId: string; - providerResult: "succeeded" | "failed" | "not_dispatched" | null; - spendOutcome: "confirmed_charged" | "confirmed_not_charged" | null; - costUsd: string | null; - evidenceRef: string; - providerRequestId: string | null; - } - | { kind: "dataforseo-doctor"; profileFlag: string | null } - | { kind: "bing-doctor"; profileFlag: string | null } - | { kind: "google-ads-doctor"; profileFlag: string | null } - | { kind: "gsc-doctor"; profileFlag: string | null } - | { kind: "posthog-doctor"; profileFlag: string | null } - | { kind: "hubspot-doctor"; profileFlag: string | null } - | { kind: "pagespeed-doctor"; profileFlag: string | null } - | { - kind: "dataforseo-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - allowSpend: boolean; - maxSpendUsd: string | null; - out: string | null; - force: boolean; - dryRun: boolean; - } - | { - kind: "bing-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - allowWrite: boolean; - out: string | null; - force: boolean; - dryRun: boolean; - } - | { - kind: "google-ads-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - out: string | null; - force: boolean; - dryRun: boolean; - } - | { - kind: "gsc-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - allowWrite: boolean; - out: string | null; - force: boolean; - dryRun: boolean; - } - | { - kind: "posthog-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - out: string | null; - force: boolean; - dryRun: boolean; - } - | { - kind: "pagespeed-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - out: string | null; - force: boolean; - dryRun: boolean; - } - | { - kind: "hubspot-call"; - profileFlag: string | null; - capabilityId: string; - input: string; - out: string | null; - force: boolean; - dryRun: boolean; - }; + | SiteCommand + | { kind: 'help' } + | { kind: 'version' } + | { kind: 'schema'; selector: string | null } + | { kind: 'skill'; path: string | null } + | { kind: 'describe'; id: string } + | { kind: 'docs'; provider: string | null } + | { kind: 'operations-list'; limit: number } + | { kind: 'operations-show'; operationId: string } + | { + kind: 'operations-reconcile' + operationId: string + providerResult: 'succeeded' | 'failed' | 'not_dispatched' | null + spendOutcome: 'confirmed_charged' | 'confirmed_not_charged' | null + costUsd: string | null + evidenceRef: string + providerRequestId: string | null + } + | { kind: 'dataforseo-doctor'; profileFlag: string | null } + | { kind: 'bing-doctor'; profileFlag: string | null } + | { kind: 'google-ads-doctor'; profileFlag: string | null } + | { kind: 'gsc-doctor'; profileFlag: string | null } + | { kind: 'posthog-doctor'; profileFlag: string | null } + | { kind: 'hubspot-doctor'; profileFlag: string | null } + | { kind: 'pagespeed-doctor'; profileFlag: string | null } + | { + kind: 'dataforseo-call' + profileFlag: string | null + capabilityId: string + input: string + allowSpend: boolean + maxSpendUsd: string | null + out: string | null + force: boolean + dryRun: boolean + } + | { + kind: 'bing-call' + profileFlag: string | null + capabilityId: string + input: string + allowWrite: boolean + out: string | null + force: boolean + dryRun: boolean + } + | { + kind: 'google-ads-call' + profileFlag: string | null + capabilityId: string + input: string + out: string | null + force: boolean + dryRun: boolean + } + | { + kind: 'gsc-call' + profileFlag: string | null + capabilityId: string + input: string + allowWrite: boolean + out: string | null + force: boolean + dryRun: boolean + } + | { + kind: 'posthog-call' + profileFlag: string | null + capabilityId: string + input: string + out: string | null + force: boolean + dryRun: boolean + } + | { + kind: 'pagespeed-call' + profileFlag: string | null + capabilityId: string + input: string + out: string | null + force: boolean + dryRun: boolean + } + | { + kind: 'hubspot-call' + profileFlag: string | null + capabilityId: string + input: string + out: string | null + force: boolean + dryRun: boolean + } -type FlagValue = string | true; +type FlagValue = string | true function invalid(message: string): never { - throw new GkitFailure({ - code: "INVALID_INPUT", - message, - hint: "Run gkit --help or gkit --schema to inspect the accepted command surface.", - }); + throw new GkitFailure({ + code: 'INVALID_INPUT', + message, + hint: 'Run gkit --help or gkit --schema to inspect the accepted command surface.', + }) } function takeGlobalProfile(argv: string[]): { - profileFlag: string | null; - rest: string[]; + profileFlag: string | null + rest: string[] } { - let profileFlag: string | null = null; - const rest: string[] = []; - for (let index = 0; index < argv.length; index++) { - const token = argv[index]!; - if (token === "--profile") { - if (profileFlag !== null) invalid("--profile may be provided only once."); - const value = argv[index + 1]; - if (!value || value.startsWith("--")) invalid("--profile requires a value."); - profileFlag = value; - index++; - continue; - } - if (token.startsWith("--profile=")) { - if (profileFlag !== null) invalid("--profile may be provided only once."); - const value = token.slice("--profile=".length); - if (!value) invalid("--profile requires a value."); - profileFlag = value; - continue; - } - rest.push(token); - } - return { profileFlag, rest }; + let profileFlag: string | null = null + const rest: string[] = [] + for (let index = 0; index < argv.length; index++) { + const token = argv[index]! + if (token === '--profile') { + if (profileFlag !== null) invalid('--profile may be provided only once.') + const value = argv[index + 1] + if (!value || value.startsWith('--')) + invalid('--profile requires a value.') + profileFlag = value + index++ + continue + } + if (token.startsWith('--profile=')) { + if (profileFlag !== null) invalid('--profile may be provided only once.') + const value = token.slice('--profile='.length) + if (!value) invalid('--profile requires a value.') + profileFlag = value + continue + } + rest.push(token) + } + return { profileFlag, rest } } -function parseFlags(argv: string[], booleanNames: ReadonlySet): Map { - const flags = new Map(); - for (let index = 0; index < argv.length; index++) { - const token = argv[index]!; - if (!token.startsWith("--")) invalid(`Unexpected argument: ${token}`); - const equal = token.indexOf("="); - const name = equal === -1 ? token : token.slice(0, equal); - if (flags.has(name)) invalid(`Duplicate flag: ${name}`); +function parseFlags( + argv: string[], + booleanNames: ReadonlySet, +): Map { + const flags = new Map() + for (let index = 0; index < argv.length; index++) { + const token = argv[index]! + if (!token.startsWith('--')) invalid(`Unexpected argument: ${token}`) + const equal = token.indexOf('=') + const name = equal === -1 ? token : token.slice(0, equal) + if (flags.has(name)) invalid(`Duplicate flag: ${name}`) - if (booleanNames.has(name)) { - if (equal !== -1) invalid(`${name} does not accept a value.`); - flags.set(name, true); - continue; - } + if (booleanNames.has(name)) { + if (equal !== -1) invalid(`${name} does not accept a value.`) + flags.set(name, true) + continue + } - const value = equal === -1 ? argv[index + 1] : token.slice(equal + 1); - if (!value || (equal === -1 && value.startsWith("--"))) { - invalid(`${name} requires a value.`); - } - flags.set(name, value); - if (equal === -1) index++; - } - return flags; + const value = equal === -1 ? argv[index + 1] : token.slice(equal + 1) + if (!value || (equal === -1 && value.startsWith('--'))) { + invalid(`${name} requires a value.`) + } + flags.set(name, value) + if (equal === -1) index++ + } + return flags } function requireOnly(flags: Map, allowed: string[]): void { - const allowedSet = new Set(allowed); - for (const name of flags.keys()) { - if (!allowedSet.has(name)) invalid(`Unknown flag: ${name}`); - } + const allowedSet = new Set(allowed) + for (const name of flags.keys()) { + if (!allowedSet.has(name)) invalid(`Unknown flag: ${name}`) + } } function requiredString(flags: Map, name: string): string { - const value = flags.get(name); - if (typeof value !== "string") invalid(`${name} is required.`); - return value; + const value = flags.get(name) + if (typeof value !== 'string') invalid(`${name} is required.`) + return value } -function optionalString(flags: Map, name: string): string | null { - const value = flags.get(name); - return typeof value === "string" ? value : null; +function optionalString( + flags: Map, + name: string, +): string | null { + const value = flags.get(name) + return typeof value === 'string' ? value : null } export function parseArgs(argv: string[]): ParsedCommand { - const { profileFlag, rest } = takeGlobalProfile(argv); - if (rest.length === 0 || rest[0] === "--help" || rest[0] === "-h") { - if (rest.length > 1) invalid("--help does not accept additional arguments."); - return { kind: "help" }; - } + const { profileFlag, rest } = takeGlobalProfile(argv) + if (rest.length === 0 || rest[0] === '--help' || rest[0] === '-h') { + if (rest.length > 1) invalid('--help does not accept additional arguments.') + return { kind: 'help' } + } - if (rest[0] === "--version") { - if (profileFlag || rest.length > 1) invalid("--version takes no other arguments."); - return { kind: "version" }; - } + if (rest[0] === '--version') { + if (profileFlag || rest.length > 1) + invalid('--version takes no other arguments.') + return { kind: 'version' } + } - if (rest[0] === "--schema" || rest[0]!.startsWith("--schema=")) { - if (profileFlag) invalid("--schema does not load a profile."); - const first = rest[0]!; - const inline = first.startsWith("--schema=") ? first.slice("--schema=".length) : null; - const selector = inline || rest[1] || null; - if (rest.length > (selector && !inline ? 2 : 1)) { - invalid("--schema accepts at most one selector."); - } - return { kind: "schema", selector }; - } + if (rest[0] === '--schema' || rest[0]!.startsWith('--schema=')) { + if (profileFlag) invalid('--schema does not load a profile.') + const first = rest[0]! + const inline = first.startsWith('--schema=') + ? first.slice('--schema='.length) + : null + const selector = inline || rest[1] || null + if (rest.length > (selector && !inline ? 2 : 1)) { + invalid('--schema accepts at most one selector.') + } + return { kind: 'schema', selector } + } - // First-token builtin matching argc's @skill contract; gkit keeps its own - // dispatcher, so the skill must be served here rather than via cli(). - if (rest[0] === "@skill") { - if (profileFlag) invalid("@skill does not load a profile."); - if (rest.length > 2) invalid("@skill takes at most one path."); - return { kind: "skill", path: rest[1] ?? null }; - } + // First-token builtin matching argc's @skill contract; gkit keeps its own + // dispatcher, so the skill must be served here rather than via cli(). + if (rest[0] === '@skill') { + if (profileFlag) invalid('@skill does not load a profile.') + if (rest.length > 2) invalid('@skill takes at most one path.') + return { kind: 'skill', path: rest[1] ?? null } + } - if (rest[0] === "describe") { - if (profileFlag) invalid("describe does not load a profile."); - const flags = parseFlags(rest.slice(1), new Set()); - requireOnly(flags, ["--id"]); - return { kind: "describe", id: requiredString(flags, "--id") }; - } + if (rest[0] === 'describe') { + if (profileFlag) invalid('describe does not load a profile.') + const flags = parseFlags(rest.slice(1), new Set()) + requireOnly(flags, ['--id']) + return { kind: 'describe', id: requiredString(flags, '--id') } + } - if (rest[0] === "docs") { - if (profileFlag) invalid("docs does not load a profile."); - const flags = parseFlags(rest.slice(1), new Set()); - requireOnly(flags, ["--provider"]); - return { kind: "docs", provider: optionalString(flags, "--provider") }; - } + if (rest[0] === 'docs') { + if (profileFlag) invalid('docs does not load a profile.') + const flags = parseFlags(rest.slice(1), new Set()) + requireOnly(flags, ['--provider']) + return { kind: 'docs', provider: optionalString(flags, '--provider') } + } - if (rest[0] === "site") return parseSiteArgs(rest.slice(1), profileFlag); + if (rest[0] === 'site') return parseSiteArgs(rest.slice(1), profileFlag) - if (rest[0] === "operations") { - if (profileFlag) invalid("operations commands do not load a profile."); - if (rest.length === 1 || rest[1] === "list" || rest[1]!.startsWith("--")) { - const flags = parseFlags(rest.slice(rest[1] === "list" ? 2 : 1), new Set()); - requireOnly(flags, ["--limit"]); - const limitText = optionalString(flags, "--limit") ?? "20"; - if (!/^[1-9]\d{0,3}$/.test(limitText)) invalid("--limit must be an integer from 1 to 9999."); - return { kind: "operations-list", limit: Number(limitText) }; - } - if (rest[1] === "show") { - const flags = parseFlags(rest.slice(2), new Set()); - requireOnly(flags, ["--id"]); - return { kind: "operations-show", operationId: requiredString(flags, "--id") }; - } - if (rest[1] !== "reconcile") invalid(`Unknown operations command: ${rest[1]}`); - const flags = parseFlags(rest.slice(2), new Set()); - requireOnly(flags, [ - "--id", - "--result", - "--spend-outcome", - "--evidence-ref", - "--cost-usd", - "--provider-request-id", - ]); - const providerResult = optionalString(flags, "--result"); - if ( - providerResult !== null && - providerResult !== "succeeded" && - providerResult !== "failed" && - providerResult !== "not_dispatched" - ) { - invalid("--result must be succeeded, failed, or not_dispatched."); - } - const spendOutcome = optionalString(flags, "--spend-outcome"); - if ( - spendOutcome !== null && - spendOutcome !== "confirmed_charged" && - spendOutcome !== "confirmed_not_charged" - ) { - invalid("--spend-outcome must be confirmed_charged or confirmed_not_charged."); - } - if (providerResult === null && spendOutcome === null) { - invalid("operations reconcile requires --result, --spend-outcome, or both."); - } - return { - kind: "operations-reconcile", - operationId: requiredString(flags, "--id"), - providerResult, - spendOutcome, - costUsd: optionalString(flags, "--cost-usd"), - evidenceRef: requiredString(flags, "--evidence-ref"), - providerRequestId: optionalString(flags, "--provider-request-id"), - }; - } + if (rest[0] === 'operations') { + if (profileFlag) invalid('operations commands do not load a profile.') + if (rest.length === 1 || rest[1] === 'list' || rest[1]!.startsWith('--')) { + const flags = parseFlags( + rest.slice(rest[1] === 'list' ? 2 : 1), + new Set(), + ) + requireOnly(flags, ['--limit']) + const limitText = optionalString(flags, '--limit') ?? '20' + if (!/^[1-9]\d{0,3}$/.test(limitText)) + invalid('--limit must be an integer from 1 to 9999.') + return { kind: 'operations-list', limit: Number(limitText) } + } + if (rest[1] === 'show') { + const flags = parseFlags(rest.slice(2), new Set()) + requireOnly(flags, ['--id']) + return { + kind: 'operations-show', + operationId: requiredString(flags, '--id'), + } + } + if (rest[1] !== 'reconcile') + invalid(`Unknown operations command: ${rest[1]}`) + const flags = parseFlags(rest.slice(2), new Set()) + requireOnly(flags, [ + '--id', + '--result', + '--spend-outcome', + '--evidence-ref', + '--cost-usd', + '--provider-request-id', + ]) + const providerResult = optionalString(flags, '--result') + if ( + providerResult !== null && + providerResult !== 'succeeded' && + providerResult !== 'failed' && + providerResult !== 'not_dispatched' + ) { + invalid('--result must be succeeded, failed, or not_dispatched.') + } + const spendOutcome = optionalString(flags, '--spend-outcome') + if ( + spendOutcome !== null && + spendOutcome !== 'confirmed_charged' && + spendOutcome !== 'confirmed_not_charged' + ) { + invalid( + '--spend-outcome must be confirmed_charged or confirmed_not_charged.', + ) + } + if (providerResult === null && spendOutcome === null) { + invalid( + 'operations reconcile requires --result, --spend-outcome, or both.', + ) + } + return { + kind: 'operations-reconcile', + operationId: requiredString(flags, '--id'), + providerResult, + spendOutcome, + costUsd: optionalString(flags, '--cost-usd'), + evidenceRef: requiredString(flags, '--evidence-ref'), + providerRequestId: optionalString(flags, '--provider-request-id'), + } + } - if ( - rest[0] !== "bing" && - rest[0] !== "dataforseo" && - rest[0] !== "google-ads" && - rest[0] !== "gsc" && - rest[0] !== "hubspot" && - rest[0] !== "pagespeed" && - rest[0] !== "posthog" - ) { - invalid(`Unknown command: ${rest[0]}`); - } - const provider = rest[0]; - if (rest[1] === "doctor" && rest.length === 2) { - return { - kind: - provider === "dataforseo" - ? "dataforseo-doctor" - : provider === "bing" - ? "bing-doctor" - : provider === "google-ads" - ? "google-ads-doctor" - : provider === "gsc" - ? "gsc-doctor" - : provider === "hubspot" - ? "hubspot-doctor" - : provider === "pagespeed" - ? "pagespeed-doctor" - : "posthog-doctor", - profileFlag, - }; - } - if (rest[1] !== "api" || rest[2] !== "call") { - invalid(`Expected ${provider} doctor or ${provider} api call.`); - } + if ( + rest[0] !== 'bing' && + rest[0] !== 'dataforseo' && + rest[0] !== 'google-ads' && + rest[0] !== 'gsc' && + rest[0] !== 'hubspot' && + rest[0] !== 'pagespeed' && + rest[0] !== 'posthog' + ) { + invalid(`Unknown command: ${rest[0]}`) + } + const provider = rest[0] + if (rest[1] === 'doctor' && rest.length === 2) { + return { + kind: + provider === 'dataforseo' + ? 'dataforseo-doctor' + : provider === 'bing' + ? 'bing-doctor' + : provider === 'google-ads' + ? 'google-ads-doctor' + : provider === 'gsc' + ? 'gsc-doctor' + : provider === 'hubspot' + ? 'hubspot-doctor' + : provider === 'pagespeed' + ? 'pagespeed-doctor' + : 'posthog-doctor', + profileFlag, + } + } + if (rest[1] !== 'api' || rest[2] !== 'call') { + invalid(`Expected ${provider} doctor or ${provider} api call.`) + } - const booleanNames = new Set(["--allow-spend", "--allow-write", "--force", "--dry-run"]); - const flags = parseFlags(rest.slice(3), booleanNames); - if (provider === "gsc" || provider === "bing") { - requireOnly(flags, [ - "--capability-id", - "--input", - "--allow-write", - "--out", - "--force", - "--dry-run", - ]); - return { - kind: provider === "gsc" ? "gsc-call" : "bing-call", - profileFlag, - capabilityId: requiredString(flags, "--capability-id"), - input: requiredString(flags, "--input"), - allowWrite: flags.get("--allow-write") === true, - out: optionalString(flags, "--out"), - force: flags.get("--force") === true, - dryRun: flags.get("--dry-run") === true, - }; - } - if (provider !== "dataforseo") { - requireOnly(flags, ["--capability-id", "--input", "--out", "--force", "--dry-run"]); - return { - kind: - provider === "posthog" - ? "posthog-call" - : provider === "hubspot" - ? "hubspot-call" - : provider === "pagespeed" - ? "pagespeed-call" - : "google-ads-call", - profileFlag, - capabilityId: requiredString(flags, "--capability-id"), - input: requiredString(flags, "--input"), - out: optionalString(flags, "--out"), - force: flags.get("--force") === true, - dryRun: flags.get("--dry-run") === true, - }; - } - requireOnly(flags, [ - "--capability-id", - "--input", - "--allow-spend", - "--max-spend-usd", - "--out", - "--force", - "--dry-run", - ]); - return { - kind: "dataforseo-call", - profileFlag, - capabilityId: requiredString(flags, "--capability-id"), - input: requiredString(flags, "--input"), - allowSpend: flags.get("--allow-spend") === true, - maxSpendUsd: optionalString(flags, "--max-spend-usd"), - out: optionalString(flags, "--out"), - force: flags.get("--force") === true, - dryRun: flags.get("--dry-run") === true, - }; + const booleanNames = new Set([ + '--allow-spend', + '--allow-write', + '--force', + '--dry-run', + ]) + const flags = parseFlags(rest.slice(3), booleanNames) + if (provider === 'gsc' || provider === 'bing') { + requireOnly(flags, [ + '--capability-id', + '--input', + '--allow-write', + '--out', + '--force', + '--dry-run', + ]) + return { + kind: provider === 'gsc' ? 'gsc-call' : 'bing-call', + profileFlag, + capabilityId: requiredString(flags, '--capability-id'), + input: requiredString(flags, '--input'), + allowWrite: flags.get('--allow-write') === true, + out: optionalString(flags, '--out'), + force: flags.get('--force') === true, + dryRun: flags.get('--dry-run') === true, + } + } + if (provider !== 'dataforseo') { + requireOnly(flags, [ + '--capability-id', + '--input', + '--out', + '--force', + '--dry-run', + ]) + return { + kind: + provider === 'posthog' + ? 'posthog-call' + : provider === 'hubspot' + ? 'hubspot-call' + : provider === 'pagespeed' + ? 'pagespeed-call' + : 'google-ads-call', + profileFlag, + capabilityId: requiredString(flags, '--capability-id'), + input: requiredString(flags, '--input'), + out: optionalString(flags, '--out'), + force: flags.get('--force') === true, + dryRun: flags.get('--dry-run') === true, + } + } + requireOnly(flags, [ + '--capability-id', + '--input', + '--allow-spend', + '--max-spend-usd', + '--out', + '--force', + '--dry-run', + ]) + return { + kind: 'dataforseo-call', + profileFlag, + capabilityId: requiredString(flags, '--capability-id'), + input: requiredString(flags, '--input'), + allowSpend: flags.get('--allow-spend') === true, + maxSpendUsd: optionalString(flags, '--max-spend-usd'), + out: optionalString(flags, '--out'), + force: flags.get('--force') === true, + dryRun: flags.get('--dry-run') === true, + } } export function renderHelp(): string { - return [ - "gkit — profile-bound growth provider CLI", - "", - "Discovery:", - " gkit --schema [selector]", - " gkit @skill [path]", - " gkit --version", - " gkit describe --id ", - " gkit docs [--provider ]", - "", - "DataForSEO:", - " gkit --profile dataforseo doctor", - " gkit --profile dataforseo api call --capability-id --input @request.json --allow-spend --max-spend-usd --out --dry-run", - " gkit --profile dataforseo api call --capability-id --input @request.json --allow-spend --max-spend-usd --out ", - " Default artifact behavior is no-replace; add --force only after reviewing the destination.", - "", - "Bing Webmaster:", - " gkit --profile bing doctor", - " gkit --profile bing api call --capability-id --input @request.json --out --dry-run", - " gkit --profile bing api call --capability-id --input @request.json --out ", - " Write capabilities (bing.urls.submit, bing.sitemaps.submit) also require --allow-write.", - "", - "PostHog:", - " gkit --profile posthog doctor", - " gkit --profile posthog api call --capability-id posthog.query.run --input @request.json --out --dry-run", - " gkit --profile posthog api call --capability-id posthog.query.run --input @request.json --out ", - "", - "Google Ads:", - " gkit --profile google-ads doctor", - " gkit --profile google-ads api call --capability-id --input @request.json --out --dry-run", - " gkit --profile google-ads api call --capability-id --input @request.json --out ", - "", - "Google Search Console:", - " gkit --profile gsc doctor", - " gkit --profile gsc api call --capability-id --input @request.json --out --dry-run", - " gkit --profile gsc api call --capability-id --input @request.json --out ", - " Write capabilities (gsc.sitemaps.submit) also require --allow-write, including --dry-run.", - "", - "HubSpot:", - " gkit --profile hubspot doctor", - " gkit --profile hubspot api call --capability-id --input @request.json --out --dry-run", - " gkit --profile hubspot api call --capability-id --input @request.json --out ", - "", - "PageSpeed Insights:", - " gkit --profile pagespeed doctor", - " gkit --profile pagespeed api call --capability-id pagespeed.run --input @request.json --out --dry-run", - "", - "Site facts:", - " gkit --schema site", - " gkit --profile site doctor", - " gkit --profile site discover --out ", - " gkit --profile site snapshot --build .output --revision --context