Skip to content

Commit 8aae70f

Browse files
committed
feat(mcp): validate every documented API response at the apiFetch boundary
The stdio CLI read every API response as `payload: any` and picked fields out by optional-chaining guesswork, so a renamed Worker field degraded silently at runtime. gen-contract-api-schemas.ts generates @loopover/contract/api-schemas from src/openapi/schemas.ts, which stays canonical and untouched: moving the schemas was tried first and rejected because zod-to-openapi's .openapi() exists only on schemas constructed after extendZodWithOpenApi runs, and both post-hoc naming routes (.openapi() clones; .meta({id}) propagates through .nullable()/.extend() differently) changed the published document. Generation with the names stripped keeps the document byte-identical by construction, with --check in test:ci like the other generators. The generator also emits CLI_RESPONSE_SCHEMAS, path -> response schema, derived from a scan of the CLI's own literal api* call sites joined against the published document -- nothing hand-listed. The scanner requires the closing delimiter: the first draft collected a template path's truncated prefix, which for a documented base path would have validated the wrong endpoint's schema. apiGet/apiPost are overloaded so a literal documented path returns the schema's inferred type, and apiFetch safeParses every documented response under #9519's recorded posture: report and pass the payload through untouched by default (returning zod's parse would strip fields the document under-describes), throw under LOOPOVER_VALIDATE_RESPONSES for CI and self-host. The warning dedupes per path so a polling loop cannot flood stderr, and lives in a hoisted accessor because the entrypoint awaits runCli before module consts this far down initialize. Typing the responses immediately surfaced real drift: /v1/local/branch-analysis returns predictedGate and dataQuality, which the document omitted entirely while two predict-gate tools read them; both are now declared (as unknown until #9531 types the verdict). The skipped-pr-audit call site moves its query out of the path template so the scanner keeps it validated. `payload: any` is extinct in the package and pinned by a grep gate test, along with the table matching the scan exactly and the unvalidated remainder as a shrink-only list. The posture tests drive the real compiled bin against a local server in both modes.
1 parent 423f269 commit 8aae70f

8 files changed

Lines changed: 1551 additions & 26 deletions

File tree

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"loadtest:worker": "node --experimental-strip-types scripts/load-test-worker.ts",
2626
"mcp:tool-reference": "tsx scripts/gen-mcp-tool-reference.ts",
2727
"mcp:tool-reference:check": "tsx scripts/gen-mcp-tool-reference.ts --check",
28+
"contract:api-schemas": "tsx scripts/gen-contract-api-schemas.ts",
29+
"contract:api-schemas:check": "tsx scripts/gen-contract-api-schemas.ts --check",
2830
"command-reference": "node --experimental-strip-types scripts/gen-command-reference.ts",
2931
"command-reference:check": "node --experimental-strip-types scripts/gen-command-reference.ts --check",
3032
"selfhost:validate-observability": "node --experimental-strip-types scripts/validate-observability-configs.ts",
@@ -124,7 +126,7 @@
124126
"test:smoke:browser:install": "playwright install chromium",
125127
"test:smoke:browser": "node --experimental-strip-types scripts/smoke-ui-browser.ts",
126128
"pretest:ci": "npm run check-node-version",
127-
"test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run build:mcp && npm run build:miner && npm run build --workspace @loopover/ui-kit && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run validate:mcp && npm run test --workspace @loopover/engine && npm run test:workers && npm run test:mcp-pack && npm run test:miner-pack && npm run test:engine-pack && npm run test:ui-kit-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:version-audit && npm run docs:drift-check && npm run coverage-boltons:check && npm run import-specifiers:check && npm run ui-derived-types:check && npm run dead-source-files:check && npm run regate-sort-key:check && npm run command-redelivery-guards:check && npm run dispatch-gate-reasons:check && npm run validate:no-hand-written-js && npm run replay-runner-manifest:check && npm run coco-dev-versions:check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run command-reference:check && npm run mcp:tool-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
129+
"test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run build:mcp && npm run build:miner && npm run build --workspace @loopover/ui-kit && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run validate:mcp && npm run test --workspace @loopover/engine && npm run test:workers && npm run test:mcp-pack && npm run test:miner-pack && npm run test:engine-pack && npm run test:ui-kit-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:version-audit && npm run docs:drift-check && npm run coverage-boltons:check && npm run import-specifiers:check && npm run ui-derived-types:check && npm run dead-source-files:check && npm run regate-sort-key:check && npm run command-redelivery-guards:check && npm run dispatch-gate-reasons:check && npm run validate:no-hand-written-js && npm run replay-runner-manifest:check && npm run coco-dev-versions:check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run command-reference:check && npm run mcp:tool-reference:check && npm run contract:api-schemas:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
128130
"test:release": "npm run test:ci && npm run changelog:check",
129131
"test:release:mcp": "npm run test:ci",
130132
"test:watch": "vitest",

packages/loopover-contract/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
"./public-api": {
5656
"types": "./dist/public-api.d.ts",
5757
"default": "./dist/public-api.js"
58+
},
59+
"./api-schemas": {
60+
"types": "./dist/api-schemas.d.ts",
61+
"default": "./dist/api-schemas.js"
5862
}
5963
},
6064
"files": [

0 commit comments

Comments
 (0)