Skip to content

test(spec): envelopeViolations — the conformance check BaseResponseSchema cannot express - #4090

Merged
os-zhuang merged 2 commits into
mainfrom
claude/envelope-drift-route-modules-v2zoky
Jul 30, 2026
Merged

test(spec): envelopeViolations — the conformance check BaseResponseSchema cannot express#4090
os-zhuang merged 2 commits into
mainfrom
claude/envelope-drift-route-modules-v2zoky

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes a gap in my own prior work on the #3843 line: the assertion those suites lead with proves less than their comments claim.

The overclaim

Every conformance suite opens with this, under a comment I wrote in #3972 and reused since:

// The contract itself, imported — not a restatement of it.
const parsed = BaseResponseSchema.safeParse(body);
expect(parsed.success, `body is not a BaseResponse: …`).toBe(true);

It isn't the contract. Demonstrably:

BaseResponseSchema.safeParse({ success: true })                     // ✅ passes
BaseResponseSchema.safeParse({ success: true, data: link, link })   // ✅ passes

BaseResponseSchema is { success, error?, meta? } — it declares no data (each response type adds its own via .extend({ data })), and a plain z.object strips unknown keys rather than rejecting them.

So it catches the one drift it was added for — a missing or non-boolean success, the flag unwrapResponse keys on, which is what #3675 / #3689 / #3843 were about — and nothing else. That second body is precisely the duplicate-payload drift /share-links shipped until #4038 / #4049 removed it, and safeParse passed it the entire time.

What was actually catching it: assertions each suite hand-wrote alongside (deadTopLevel, expectData, the ENVELOPE_KEYS loop I wrote once in domain-handler-registry.test.ts). That works exactly as long as whoever writes the next suite remembers to.

envelopeViolations(body)

Returns every departure as readable reasons; empty means conformant.

rule why
success must be a boolean the flag unwrapResponse keys on; without it a body is handed to callers raw
a success body must carry data undefined only — null, [], {}, 0, '' are payloads, not absences
a failure body must carry error with string code + message the nested form, not the pre-#3675 bare string
no top-level key outside success/data/error/meta the general form of the duplicate-payload drift

It deliberately does not check the shape of data. That's each route's own payload schema, and conflating the two is what let SettingsNamespacePayload describe a whole body before #3843 and only data after it.

Placed in contract.zod.ts beside the schema it completes, alongside the plain predicates spec/api already exports (standardErrorCodeForHttpStatus, readServiceSelfInfo). No new package — and BaseResponseSchema itself is untouched, because adding data or .strict() would hit every .extend() caller and every producer for no gain over fixing this in the assertions.

Verification

Two directions, because either alone would be misleading.

The pairing is load-bearing. I reintroduced the exact /share-links drift ({ success: true, data, link: data }). The new assertion fails with stray top-level key link— the payload belongs underdata``; the old safeParse line still passes. That contrast is the argument for the change.

The rules aren't too strict. Every existing suite runs unchanged and green — nothing legitimate is flagged:

package result
@objectstack/spec 6951 passed (incl. 14 new for the predicate)
@objectstack/rest 505 passed
@objectstack/runtime 914 passed
@objectstack/client 200 passed
storage · settings · datasource · i18n · sharing 220 · 189 · 154 · 62 · 225
spec tsc --noEmit · check:docs · check:skill-refs · check:react-blocks clean
check:route-envelope · check:error-code-casing · check:console-sha · check:nul-bytes · check:role-word · check:org-identifier clean
pnpm lint 0 errors in source (see note)

Two local-artifact notes, both of which look like failures and aren't:

  • pnpm lint reports 61 errors — all inside .cache/objectui-96ee72e85439/, the gitignored console build artifact my earlier .objectui-sha bump left in this worktree. Zero in real source; CI checks out fresh and never sees it.
  • The first rest run showed 8 anonymous-deny 401 failures. That's the stale-dist signature I flagged in fix(plugin-sharing)!: the share-link routes emit the declared envelope, and the last ratchet retires (#3983) #4037's method note — I'd rebuilt only spec, not its dependents. A full turbo run build cleared it (505/505). Same for the recurring runtime datasource-autoconnect failure: gitignored .objectstack/data/memory-driver.json accumulating rows across local runs, 914/914 once removed.

Generated by Claude Code

…hema cannot express

Every conformance suite from the #3843 line leads with
`BaseResponseSchema.safeParse(body)` under a comment calling it "the contract
itself, imported — not a restatement of it". That overclaimed, and the gap is
demonstrable:

  BaseResponseSchema.safeParse({ success: true })                    // passes
  BaseResponseSchema.safeParse({ success: true, data: link, link })  // passes

The schema declares no `data` (each response type adds its own via
`.extend({ data })`) and a plain z.object strips unknown keys rather than
rejecting them. So it catches the one drift it was added for — a missing or
non-boolean `success`, the flag `unwrapResponse` keys on — and nothing else. The
second body is exactly the duplicate-payload drift /share-links shipped until
#4038/#4049 removed it, and safeParse passed it the whole time. What was actually
catching that drift was the assertions each suite hand-wrote beside it, which
works only for as long as whoever writes the next suite remembers to.

`envelopeViolations(body)` returns every departure as readable reasons, empty when
conformant: `success` must be a boolean; a success body must carry `data`
(`undefined` only — null/[]/{}/0/'' are payloads, not absences); a failure body
must carry `error` with a string code and message; and no top-level key outside
success/data/error/meta, which is the general form of the duplicate-payload drift.
It deliberately does NOT check the shape of `data` — that is each route's own
payload schema, and conflating the two is what let SettingsNamespacePayload
describe a whole body before #3843 and only `data` after it.

Ten suites now assert it beside safeParse, and their comments say what each of the
two actually proves. Verified the pairing is load-bearing: reintroducing the
/share-links duplicate key fails the new assertion and still passes the old one.
Verified the rules are not too strict by running every existing suite unchanged —
spec 6951, rest 505, runtime 914, storage 220, settings 189, datasource 154,
i18n 62, sharing 225, client 200, all green, nothing flagged.

Placed in contract.zod.ts beside the schema it completes, alongside the other
plain predicates spec/api already exports (standardErrorCodeForHttpStatus,
readServiceSelfInfo). No new package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 9:34am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/plugin-sharing, @objectstack/rest, @objectstack/runtime, packages/services, @objectstack/spec.

116 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/runtime, @objectstack/spec)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/runtime, packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/plugins/plugin-sharing, packages/runtime, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-sharing, @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-sharing, @objectstack/rest, @objectstack/runtime, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/plugins/plugin-sharing, packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/rest, @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

`check:api-surface` is a ratchet on `@objectstack/spec`'s public exports, and the
new predicate is an addition to it:

    ./api
      + envelopeViolations (function)
    0 breaking (removed/narrowed), 1 added.

One line, alphabetically beside the other predicates that module already exports.

Two things this caught that are worth writing down.

The gate lives in the TypeScript Type Check job, which runs TEN steps — I had
found four of them by grepping a window of lint.yml that cut off before the rest,
and ran only those. The full list is: spec tsc, check:docs, check:skill-refs,
check:react-blocks, a workspace build, examples typecheck, downstream-contract
typecheck, check:api-surface, check:skill-examples, and the two i18n checks. All
ten pass locally now.

And `gen:api-surface` reads `dist/index.d.ts`, so it fails outright under
`OS_SKIP_DTS=1` — the flag I had been using to keep local builds fast. Rebuilding
spec with declarations is a prerequisite for regenerating the snapshot, not an
optional speed-up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z

Copy link
Copy Markdown
Contributor Author

TypeScript Type Check went red on check:api-surface; fixed in 04debd7.

The gate is a ratchet on @objectstack/spec's public exports, and the new predicate is an addition to it:

  ./api
    + envelopeViolations (function)

@objectstack/spec public API changed: 0 breaking (removed/narrowed), 1 added.

Correctly classified — additive, nothing removed or narrowed. Regenerated with gen:api-surface: one line, alphabetically beside getAuthEndpointUrl / readServiceSelfInfo / standardErrorCodeForHttpStatus, the predicates that module already exports.

Two things worth recording, because both are the kind of trap that reads as "my change broke something" when it isn't.

The job runs ten steps, not the four I ran. I found the gates by grepping a window of lint.yml that ended before the rest of the job. The full list:

spec tsc --noEmit · check:docs · check:skill-refs · check:react-blocks · workspace build · examples typecheck · downstream-contract typecheck · check:api-surface · check:skill-examples · check:i18n + check:i18n-coverage

All ten pass locally now. This is the same failure mode as #3972, where I missed check:docs because it lives in the typecheck job rather than the test job — I fixed the symptom then and not the habit, which was to enumerate the job rather than grep for a name.

gen:api-surface cannot run under OS_SKIP_DTS=1. It resolves the module symbol from dist/index.d.ts, so with declarations skipped it fails with Could not resolve module symbol for . — Is the package built?, which reads like a broken build rather than a missing flag. Rebuilding spec with declarations is a prerequisite for regenerating the snapshot, not an optional speed-up.

Nothing about the change itself moved — the predicate, the suites, and the comments are as reviewed.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 09:46
@os-zhuang
os-zhuang merged commit dc530b4 into main Jul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/envelope-drift-route-modules-v2zoky branch July 30, 2026 09:46
os-zhuang added a commit that referenced this pull request Jul 30, 2026
#4090 follow-up) (#4105)

* test(runtime): the dispatcher envelope check uses the shared predicate (#4090 follow-up)

`domain-handler-registry.test.ts` hand-rolled "no key beside the envelope's own
may hold the payload" for #4038. #4090 extracted that rule into
`envelopeViolations` so it stops having two definitions; this is the caller that
motivated the extraction, now using it.

Leaving the local copy would have recreated the exact failure this line has been
closing — one rule, two places, and only one of them updated next time. It also
was not the same rule: the local set allowed any body whose top-level keys were
success/data/meta, so it passed a success body with NO `data` at all, and one
carrying an `error` alongside `success: true`. The shared predicate rejects both.

Verified the swap is a strict improvement rather than a refactor, by breaking the
producer two ways and checking the suite catches each:

  { success: true, data: link, link }   → caught (the old check caught this too)
  { success: true }                     → caught (the old check PASSED this)

runtime 914 passed. All ten gates in the TypeScript Type Check job pass, plus the
six `check:*` guards.

Note for the next person touching spec: after restarting a branch onto a newer
main, `check:api-surface` and `check:i18n-coverage` both fail against a stale
`packages/spec/dist` — the first reports other people's exports as added/removed,
the second rejects an example config for a chart type the fresh spec allows.
Neither is a real failure; rebuilding spec clears both. They read as "my change
broke something" and are not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z

* chore: add the empty changeset this PR needs

Check Changeset requires one on every PR; an empty-frontmatter changeset is the
sanctioned form for a change that releases nothing, and the gate's own error
message says so.

Second time this session — #4009 was a comment-only PR that failed the same way.
I fixed that one and did not draw the rule, which is that "this releases nothing"
is a reason to write an EMPTY changeset, not a reason to skip the file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants