From ded02964f4ed16b2b352c820a9fe5a029bbc11d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 17:28:38 +0000 Subject: [PATCH 1/2] chore(deps): bump zod from 4.4.3 to 4.6.5 Bumps [zod](https://github.com/colinhacks/zod) from 4.4.3 to 4.6.5. - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](https://github.com/colinhacks/zod/compare/v4.4.3...v4.6.5) --- updated-dependencies: - dependency-name: zod dependency-version: 4.6.5 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- apps/api/package.json | 2 +- packages/app-kit/package.json | 2 +- packages/shared/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 3b879cc2..4ef54a47 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -34,7 +34,7 @@ "socket.io": "^4.8.3", "tsx": "^4.23.15", "web-push": "^3.6.7", - "zod": "^4.4.3" + "zod": "^4.6.5" }, "devDependencies": { "@types/bcryptjs": "^3.0.0", diff --git a/packages/app-kit/package.json b/packages/app-kit/package.json index 7c611c60..0a4c2b21 100644 --- a/packages/app-kit/package.json +++ b/packages/app-kit/package.json @@ -25,7 +25,7 @@ "README.md" ], "dependencies": { - "zod": "4.4.3" + "zod": "4.6.5" }, "devDependencies": { "@types/node": "^26.6.2", diff --git a/packages/shared/package.json b/packages/shared/package.json index 11f389fd..81330844 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -16,7 +16,7 @@ "./rich-text": "./src/rich-text.ts" }, "dependencies": { - "zod": "^4.4.3" + "zod": "^4.6.5" }, "devDependencies": { "@types/node": "^26.6.2", From 44dd67b265f271a9b8f894bcd649c38b79d34cd7 Mon Sep 17 00:00:00 2001 From: Maneek21 <208369276+Maneek21@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:19:23 +0530 Subject: [PATCH 2/2] fix: preserve frozen diagnostics on Zod 4.6 --- ...9-23-dependency-compatibility-followups.md | 33 ++----- .../2026-09-23-zod-contract-compatibility.md | 44 +++++++++ .../scripts/build-module-validator.mjs | 2 + packages/app-kit/src/index.ts | 5 + .../src/module-contract/zod-compat.d.ts | 3 + .../test/validation-compatibility.test.ts | 92 +++++++++++++++++++ packages/shared/src/modules.ts | 5 + packages/shared/src/zod-compat.ts | 18 ++++ pnpm-lock.yaml | 41 ++++----- pnpm-workspace.yaml | 3 - 10 files changed, 197 insertions(+), 49 deletions(-) create mode 100644 docs/superpowers/plans/2026-09-23-zod-contract-compatibility.md create mode 100644 packages/app-kit/src/module-contract/zod-compat.d.ts create mode 100644 packages/app-kit/test/validation-compatibility.test.ts create mode 100644 packages/shared/src/zod-compat.ts diff --git a/docs/superpowers/plans/2026-09-23-dependency-compatibility-followups.md b/docs/superpowers/plans/2026-09-23-dependency-compatibility-followups.md index 7e55d40c..6968de0a 100644 --- a/docs/superpowers/plans/2026-09-23-dependency-compatibility-followups.md +++ b/docs/superpowers/plans/2026-09-23-dependency-compatibility-followups.md @@ -2,33 +2,16 @@ Reviewed against GitHub master `9e6fca0a2ea400822bcb3c8380415025a1382d1c` on 2026-09-23. -## Zod 4.6.5: separate from PR #335 +## Zod 4.6.5: compatibility fix in PR #341 -The original 19-update batch changes Zod 4.4.3 to 4.6.5. The focused App Kit -test `preserves direct v0 rejection issues instead of wrapping them in a v1 -union error` fails: verification now adds a custom issue at -`manifest.navigation[0].module_id` alongside the existing `too_small` and -`unrecognized_keys` issues. The protocol explicitly freezes rejection issue -shapes. Do not weaken that assertion as part of routine maintenance. +The original upgrade changed frozen unknown-key and union diagnostics. The +reviewed adapter restores abort behavior at the App/Module refinement +boundaries, retaining every error and preserving semantic rejection. The +workspace override is removed; Zod remains outside the routine update group. -Keep the direct API, shared and App Kit Zod versions at their master baseline -and exclude Zod from Dependabot's patch/minor group so it receives separate -review. A workspace override also holds transitive MCP and lint dependencies -at 4.4.3; remove it as part of the reviewed compatibility update. This is not a -security exception: the dependency audit reports no known vulnerabilities. - -Re-entry criteria: - -1. Reproduce with `pnpm --filter @deft/app-kit exec tsx --test - --test-name-pattern 'preserves direct v0 rejection' test/app-kit.test.ts`. -2. Compare old/new diagnostics for all frozen v0/v1/v2 malformed manifests and - packages, including unknown keys, missing modules, duplicate identities and - invalid navigation. Confirm accepted/rejected inputs remain unchanged. -3. Preserve the existing public error contract with a reviewed compatibility - adapter, or explicitly version any intended contract change. Do not filter - away validation errors or alter authority checks merely to pass the test. -4. Pass App Kit contracts, API/shared tests, workspace typecheck, web lint, - build, audit and required CI before merging the independent update. +See [the compatibility decision](2026-09-23-zod-contract-compatibility.md) for +the options, trust boundaries, rollback and validation. Future updates must +pass the committed diagnostics, package and packed-consumer regression tests. ## ESLint 10: PR #322 remains draft diff --git a/docs/superpowers/plans/2026-09-23-zod-contract-compatibility.md b/docs/superpowers/plans/2026-09-23-zod-contract-compatibility.md new file mode 100644 index 00000000..0ead58d5 --- /dev/null +++ b/docs/superpowers/plans/2026-09-23-zod-contract-compatibility.md @@ -0,0 +1,44 @@ +# Zod contract compatibility + +## Decision + +Upgrade the workspace to Zod 4.6.5 while preserving the App v0/v1/v2 and +Module v1/v2 public validation contracts. Remove the temporary 4.4.3 override. +Keep Zod outside the routine Dependabot group so future updates receive +contract validation independently. + +Zod 4.6.5 makes `unrecognized_keys` issues continuable. In 4.4.3 they aborted +refinements and affected which union diagnostics were returned. A direct +upgrade changed 241 of 1,609 comparison cases, including nested unknown keys, +duplicate identities and invalid references. Valid inputs in that corpus did +not change. + +Use one shared `abortOnUnknownContractKeys` helper at the existing App, +developer-compatibility, Module and collection refinement boundaries. It +retains every issue, restores `continue: false` for unknown-key failures, and +returns before semantic refinements run. The same helper ships in the portable +App Kit through the existing authoritative-source build. This preserves union +errors as well as the refinement behavior; guarding individual semantic rules +alone would not preserve union diagnostics. + +Keeping the old workspace pin would postpone the upgrade. Changing the frozen +protocol or accepting new diagnostics would impose a compatibility change on +consumers. The small adapter preserves the published behavior without changing +accepted inputs, removing errors, patching Zod internals, or changing schema +construction methods available to callers. + +## Boundaries and validation + +Author-supplied JSON still passes the same strict structural and semantic +schemas before packaging or host use. Parsing still grants no authority; no +tenant, approval, persistence or runtime execution boundaries change. There +is no data migration. Reverting the PR restores the previous dependency pin +and validators without transforming stored data. + +The 1,609-case before/after comparison covers valid and malformed example App +and Module manifests and matches after the adapter. Committed regression tests +cover root and nested unknown keys, duplicate identities, union branch errors, +and host/portable Kit parity. Existing package, canonicalization, protocol, +negative-corpus and packed-consumer tests remain unchanged. Run the complete +App Kit suite, workspace typecheck/build, lint, dependency audit and required +CI before merging. Future Zod upgrades must pass these same gates. diff --git a/packages/app-kit/scripts/build-module-validator.mjs b/packages/app-kit/scripts/build-module-validator.mjs index f834344e..3db4dc03 100644 --- a/packages/app-kit/scripts/build-module-validator.mjs +++ b/packages/app-kit/scripts/build-module-validator.mjs @@ -19,6 +19,7 @@ async function transpileAuthoritativeSource(name) { throw new Error(`Expected authoritative Module schema import in ${sourcePath}`); } source = source.replace(originalImport, portableImport); + source = source.replace("from './zod-compat'", "from './zod-compat.js'"); } const result = ts.transpileModule(source, { @@ -50,4 +51,5 @@ async function transpileAuthoritativeSource(name) { } await transpileAuthoritativeSource('schemas'); +await transpileAuthoritativeSource('zod-compat'); await transpileAuthoritativeSource('modules'); diff --git a/packages/app-kit/src/index.ts b/packages/app-kit/src/index.ts index 62898f07..338a551a 100644 --- a/packages/app-kit/src/index.ts +++ b/packages/app-kit/src/index.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { abortOnUnknownContractKeys } from './module-contract/zod-compat.js'; import { classifyAppAutomationOccurrence, resolveAppAutomationOccurrence, @@ -69,6 +70,7 @@ export const DeftAppDeveloperCompatibilitySchema = z.strictObject({ '2': DeftAppDeveloperProtocolV2FlowSchema.optional(), }), }).superRefine((value, ctx) => { + if (abortOnUnknownContractKeys(ctx)) return; if (new Set(value.app_kit.versions).size !== value.app_kit.versions.length) { ctx.addIssue({ code: 'custom', path: ['app_kit', 'versions'], message: 'App Kit versions must be unique' }); } @@ -258,6 +260,7 @@ export const DeftAppManifestV0Schema = z .default([]), }) .superRefine((manifest, ctx) => { + if (abortOnUnknownContractKeys(ctx)) return; const identities = new Set(); const moduleIds = new Set(); const paths = new Set(); @@ -623,6 +626,7 @@ export const DeftAppManifestV1Schema = z actions: z.array(DeftAppActionBindingV1Schema).min(1).max(APP_LIMITS.actions), }) .superRefine((manifest, ctx) => { + if (abortOnUnknownContractKeys(ctx)) return; const moduleIdentities = new Set(); const moduleIds = new Set(); const modulePaths = new Set(); @@ -761,6 +765,7 @@ export const DeftAppManifestV2Schema = z .max(APP_LIMITS.automation_requests), }) .superRefine((manifest, ctx) => { + if (abortOnUnknownContractKeys(ctx)) return; const { automation_requests: _automationRequests, ...v1Fields } = manifest; const v1Validation = DeftAppManifestV1Schema.safeParse({ ...v1Fields, diff --git a/packages/app-kit/src/module-contract/zod-compat.d.ts b/packages/app-kit/src/module-contract/zod-compat.d.ts new file mode 100644 index 00000000..15bc8e56 --- /dev/null +++ b/packages/app-kit/src/module-contract/zod-compat.d.ts @@ -0,0 +1,3 @@ +import type { z } from 'zod'; + +export function abortOnUnknownContractKeys(payload: z.RefinementCtx): boolean; diff --git a/packages/app-kit/test/validation-compatibility.test.ts b/packages/app-kit/test/validation-compatibility.test.ts new file mode 100644 index 00000000..d6e151f5 --- /dev/null +++ b/packages/app-kit/test/validation-compatibility.test.ts @@ -0,0 +1,92 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { test } from 'node:test'; +import { + DEFT_APP_DEVELOPER_COMPATIBILITY, + DeftAppDeveloperCompatibilitySchema, + parseDeftAppManifest, + validateDeftModuleManifest, +} from '../dist/index.js'; +import { parseSupportedDeftModuleManifest } from '../../shared/src/modules.js'; +import { validEquipmentModule } from './fixtures/module-semantic-negative-corpus.js'; + +const root = resolve(import.meta.dirname, '../../..'); + +for (const example of [ + 'hello-workspace-app', + 'connected-resource-campaigns-app', + 'scheduled-connected-resource-campaigns-app', +]) { + for (const path of [[], ['modules', 0], ['navigation', 0]]) { + test(`${example}: unknown keys preserve rejection issues at ${path.join('.') || 'root'}`, () => { + const manifest = JSON.parse(readFileSync(resolve(root, 'examples', example, 'deft.app.json'), 'utf8')); + assert.doesNotThrow(() => parseDeftAppManifest(manifest)); + manifest.modules.push(structuredClone(manifest.modules[0])); + assert.throws(() => parseDeftAppManifest(manifest), (error: unknown) => { + assert.ok(error && typeof error === 'object' && 'issues' in error); + assert.ok((error.issues as { code: string }[]).some(issue => issue.code === 'custom')); + return true; + }); + let object = manifest; + for (const key of path) object = object[key]; + object.runtime = {}; + assert.throws(() => parseDeftAppManifest(manifest), (error: unknown) => { + assert.ok(error && typeof error === 'object' && 'issues' in error); + assert.deepEqual(error.issues, [{ + code: 'unrecognized_keys', keys: ['runtime'], path, + message: 'Unrecognized key: "runtime"', + }]); + return true; + }); + }); + } +} + +for (const schemaVersion of ['1', '2']) { + for (const path of [[], ['collections', 0], ['collections', 0, 'fields', 0]]) { + test(`Module ${schemaVersion}: unknown keys retain host/Kit parity at ${path.join('.') || 'root'}`, () => { + const manifest: Record = structuredClone(validEquipmentModule); + manifest.schema_version = schemaVersion; + assert.doesNotThrow(() => parseSupportedDeftModuleManifest(manifest)); + manifest.collections.push(structuredClone(manifest.collections[0])); + assert.throws(() => parseSupportedDeftModuleManifest(manifest)); + let object = manifest; + for (const key of path) object = object[key]; + object.runtime = {}; + assert.throws(() => parseSupportedDeftModuleManifest(manifest), (error: unknown) => { + assert.ok(error && typeof error === 'object' && 'issues' in error); + // Both branches aborted on unknown keys before Zod 4.6; the public + // union diagnostic and branch ordering are part of that contract. + assert.deepEqual(error.issues, [{ + code: 'invalid_union', path: [], message: 'Invalid input', + errors: ['1', '2'].map(version => [ + ...(version === schemaVersion ? [] : [{ + code: 'invalid_value', values: [version], path: ['schema_version'], + message: `Invalid input: expected "${version}"`, + }]), + { code: 'unrecognized_keys', keys: ['runtime'], path, + message: 'Unrecognized key: "runtime"' }, + ]), + }]); + return true; + }); + const result = validateDeftModuleManifest(manifest); + assert.equal(result.success, false); + assert.equal(result.issues.length, 1); + assert.equal(result.issues[0]?.reason, 'Invalid input'); + }); + } +} + +test('developer compatibility keeps unknown-key diagnostics without accepting duplicate versions', () => { + const value = structuredClone(DEFT_APP_DEVELOPER_COMPATIBILITY); + const invalid = { ...value, app_kit: { ...value.app_kit, versions: [...value.app_kit.versions, value.app_kit.versions[0]] } }; + assert.equal(DeftAppDeveloperCompatibilitySchema.safeParse(invalid).success, false); + const result = DeftAppDeveloperCompatibilitySchema.safeParse({ ...invalid, runtime: {} }); + assert.equal(result.success, false); + if (!result.success) assert.deepEqual(result.error.issues, [{ + code: 'unrecognized_keys', keys: ['runtime'], path: [], + message: 'Unrecognized key: "runtime"', + }]); +}); diff --git a/packages/shared/src/modules.ts b/packages/shared/src/modules.ts index 3a77ee97..e551de1f 100644 --- a/packages/shared/src/modules.ts +++ b/packages/shared/src/modules.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { abortOnUnknownContractKeys } from './zod-compat'; import { SEMVER_REGEX } from './schemas'; /** @@ -496,6 +497,7 @@ export const ModuleCollectionSchema = z latest_related: z.array(ModuleRelatedLatestSchema).max(4).optional(), }) .superRefine((collection, ctx) => { + if (abortOnUnknownContractKeys(ctx)) return; addDuplicateIssues( collection.fields.map((field) => field.key), ['fields'], @@ -674,6 +676,7 @@ export const ModuleCollectionV2Schema = z latest_related: z.array(ModuleRelatedLatestSchema).max(4).optional(), }) .superRefine((collection, ctx) => { + if (abortOnUnknownContractKeys(ctx)) return; // Reuse the exact v1 collection invariants by projecting resource refs to // the already non-inline v1 relation shape. This keeps defaults, views, // search restrictions, duplicate checks, and all scalar behavior aligned. @@ -736,6 +739,7 @@ export const DeftModuleManifestV1Schema = z navigation: ModuleNavigationSchema.optional(), }) .superRefine((manifest, ctx) => { + if (abortOnUnknownContractKeys(ctx)) return; validateLatestRelated(manifest.collections, ctx); addDuplicateIssues( manifest.collections.map((collection) => collection.key), @@ -796,6 +800,7 @@ export const DeftModuleManifestV2Schema = z navigation: ModuleNavigationSchema.optional(), }) .superRefine((manifest, ctx) => { + if (abortOnUnknownContractKeys(ctx)) return; validateLatestRelated(manifest.collections, ctx); addDuplicateIssues( manifest.collections.map((collection) => collection.key), diff --git a/packages/shared/src/zod-compat.ts b/packages/shared/src/zod-compat.ts new file mode 100644 index 00000000..2b7461a4 --- /dev/null +++ b/packages/shared/src/zod-compat.ts @@ -0,0 +1,18 @@ +import type { z } from 'zod'; + +/** + * App/Module protocols freeze Zod 4.4's unknown-key diagnostics, including + * refinement short-circuiting and union branch errors. Zod 4.6 makes these + * issues continuable. Retain every issue, but restore its abort flag before + * downstream checks or containing unions inspect the parse result. + */ +export function abortOnUnknownContractKeys(payload: z.RefinementCtx): boolean { + let aborted = false; + for (const [index, issue] of payload.issues.entries()) { + if (issue.code === 'unrecognized_keys') { + payload.issues[index] = { ...issue, continue: false }; + aborted = true; + } + } + return aborted; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5088a4a..429cc533 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,6 @@ settings: excludeLinksFromLockfile: false overrides: - zod: 4.4.3 '@hono/node-server': ^2.0.12 body-parser: ^2.3.0 brace-expansion@<2.0.0: ^1.1.18 @@ -41,7 +40,7 @@ importers: dependencies: '@anthropic-ai/sdk': specifier: ^0.127.0 - version: 0.127.0(zod@4.4.3) + version: 0.127.0(zod@4.6.5) '@deft/app-kit': specifier: workspace:* version: link:../../packages/app-kit @@ -97,8 +96,8 @@ importers: specifier: ^3.6.7 version: 3.6.7 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: ^4.6.5 + version: 4.6.5 devDependencies: '@types/bcryptjs': specifier: ^3.0.0 @@ -312,8 +311,8 @@ importers: packages/app-kit: dependencies: zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.5 + version: 4.6.5 devDependencies: '@types/node': specifier: ^26.6.2 @@ -372,8 +371,8 @@ importers: packages/shared: dependencies: zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: ^4.6.5 + version: 4.6.5 devDependencies: '@types/node': specifier: ^26.6.2 @@ -395,7 +394,7 @@ packages: resolution: {integrity: sha512-OKiVbG9LHCxjn4fBJZJFy/Bz3hqE1RYpwaeSEDNheQnvz0XNKmGT/Lhkz2Sf1a27Ldt3vVv+BkU81I0U/8EpoQ==} hasBin: true peerDependencies: - zod: 4.4.3 + zod: ^3.25.0 || ^4.0.0 peerDependenciesMeta: zod: optional: true @@ -3957,10 +3956,10 @@ packages: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} peerDependencies: - zod: 4.4.3 + zod: ^3.25.0 || ^4.0.0 - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zod@4.6.5: + resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -3969,12 +3968,12 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@anthropic-ai/sdk@0.127.0(zod@4.4.3)': + '@anthropic-ai/sdk@0.127.0(zod@4.6.5)': dependencies: json-schema-to-ts: 3.1.1 standardwebhooks: 1.1.1 optionalDependencies: - zod: 4.4.3 + zod: 4.6.5 '@babel/code-frame@7.29.7': dependencies: @@ -4510,11 +4509,11 @@ snapshots: eventsource-parser: 3.0.6 jose: 6.2.2 pkce-challenge: 5.0.1 - zod: 4.4.3 + zod: 4.6.5 '@modelcontextprotocol/core@2.0.0': dependencies: - zod: 4.4.3 + zod: 4.6.5 '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: @@ -5850,8 +5849,8 @@ snapshots: '@babel/parser': 7.29.8 eslint: 9.39.5(jiti@2.7.0) hermes-parser: 0.25.1 - zod: 4.4.3 - zod-validation-error: 4.0.2(zod@4.4.3) + zod: 4.6.5 + zod-validation-error: 4.0.2(zod@4.6.5) transitivePeerDependencies: - supports-color @@ -7900,10 +7899,10 @@ snapshots: yocto-queue@0.1.0: {} - zod-validation-error@4.0.2(zod@4.4.3): + zod-validation-error@4.0.2(zod@4.6.5): dependencies: - zod: 4.4.3 + zod: 4.6.5 - zod@4.4.3: {} + zod@4.6.5: {} zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e10728f7..5afcb2c0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,9 +3,6 @@ packages: - 'packages/*' - '!apps/web-demo' overrides: - # Preserve frozen App Kit diagnostics until the Zod compatibility review. - # See docs/superpowers/plans/2026-09-23-dependency-compatibility-followups.md. - zod: 4.4.3 '@hono/node-server': ^2.0.12 body-parser: ^2.3.0 'brace-expansion@<2.0.0': ^1.1.18