Skip to content

feat(spec)!: close ApiEndpointSchema against undeclared keys, and name the author state on the upgrade path (#5384, #5227) - #6840

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-5384-api-endpoint-strict
Aug 9, 2026
Merged

feat(spec)!: close ApiEndpointSchema against undeclared keys, and name the author state on the upgrade path (#5384, #5227)#6840
os-zhuang merged 4 commits into
mainfrom
claude/issue-5384-api-endpoint-strict

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Fixes#5384
Fixes#5227

Two cards on one surface (packages/spec/src/api/endpoint.zod.ts), maintainer-batched in the v17 window ruling. One landed as a schema change; the other's type half had already landed upstream, so what remains of it here is the documentation half — itemized per card below.

Ruling chain (quoted verbatim, untranslated)

The 2026-08-05 "GA 后实施" hold was superseded by the maintainer's 2026-08-07 window re-ruling:

取代 2026-08-05「GA 后实施」条款,进 v17 窗口。#5309(信封/正文分离,硬前置)同批 …… 收紧姿态方向(正文 A vs B)在实施时按 #4001 常规套路 + 存量测量定,倾向 A。

The hard prerequisite #5309 landed in PR #6576 (merged 2026-08-08T06:10:55Z) and is in this branch's base.

#5227's triage ruled the fix direction as:

作者面暴露/指向 ApiEndpointInput(与 ApiEndpointSchema.createz.input 的既有做法一致,类型面加法不动 schema)+ 升级指南补一句可用的类型注解写法

That first clause is now impossible, and the reason is recorded below — it is not a scope reduction taken on my own authority.


Corpus measurement (the ruling made direction A conditional on it)

Direction A carried: zeroapis: entries carry an undeclared key, so no key had to be preserved and direction B does not apply.

CorpusCommand / filter directionapis: entriesUndeclared keys
examples/*grep -rn "apis:" examples/ → the one declaration site, keys read against the schema's 20 declared keys20
cloud (objectstack-ai/cloud)git -C /home/user/cloud fetch origin main --deepen=100, then git -C … grep -l … origin/main for apis:, ApiEndpoint, authRequired, objectParams, inputMapping00
  • The two example endpoints are TaskFeedEndpoint / InquiryPurgeEndpoint in examples/app-showcase/src/system/apis/index.ts; they use only declared keys (name, path, method, summary, description, type, target, objectParams, authRequired, cacheTtl).
  • cloud was reachable, and 0 is a measurement, not an unreachability.origin/main = 781fd036; all five greps returned zero files. Grepped against origin/main, never the working tree.

Card 1 — #5384: ApiEndpointSchema is closed

Item落点BeforeAfter
1packages/spec/src/api/endpoint.zod.tsopen z.object, no catchallstrictObject — catchall never, with curated aliases + guidance
2same, guidance.namespacenamespace parsed green, gated nothingrefused by name, ADR-0121 D2 wrong-layer prescription, rename suppressed
3same, guidance × 7 bookkeeping keyssilently strippedrefused with the #5309 storage-bookkeeping prescription
4packages/spec/src/kernel/metadata-type-schemas.test.tsSTILL_STRIP = ['view','api'], closed 24 of 26STILL_STRIP = ['view'], closed 25 of 26
5packages/cli/test/metadata-type-schema-gate.test.tsapi in NOT_YET_CLOSED ("both gates agree, author still told")api in GATED_AT ("both gates REJECT"); table now empty, emptiness asserted
6packages/metadata/src/publish-endpoint-gate.test.tsnamespace fixture asserted the gate ignores the keyasserts the key is refused by name, + a new non-vacuity CONTROL
7docs/audits/…-strictness-ledger.mdapi/ = wire — tolerant by designsplit: endpoint.zod.ts authorable, rest wire
8packages/metadata/src/stored-envelope.test.tsnew drift pin: every STORED_ENVELOPE_KEYS entry carries the prescription

The order is the record. Closing this shape was measured and refused first (2026-08-05): the same schema parsed STORED rows, so a naked strictObject failed every row with unrecognized_keys: ['packageId', 'state'] — the load-time backstop excluded the endpoint (404) and the publish gate reported a schema error in place of the ADR-0121 D6 verdict it exists to give. The debt was real and it was not in this vocabulary, so #5309 paid it at the layer that owned it (peelStoredEnvelope). ApiEndpointSchema never learned a bookkeeping key.

The drift pin earned its place on its first run.STORED_ENVELOPE_KEYS has seven entries and my guidance table had six — package was missing. That drift is silent in the direction that matters: the key is peeled correctly on the stored paths, so every other test stays green, while a hand-authoring author gets a generic "unrecognized key" instead of the upgrade. Fixed in the second commit.

Rejection cases assert the named verdict, never a bare throw

ApiEndpointSchema.safeParse speaks in zod issues, not an ADR-0112 { code, status } envelope — that envelope appears further out, where PUT /meta/api/:name turns this verdict into a 422. So every new rejection case asserts issue.code === 'unrecognized_keys', the offending key in issue.keys, and the prescription text. publishPackage speaks in validationErrors[], so its cases assert the named entry (name, type, message content) — the #6576 precedent. No case relies on toThrow().

Card 2 — #5227: premise expired upstream; only the guide half was left to build

premise_still_valid: false for the type half. The issue's fact — "export type ApiEndpoint = z.infer 是输出类型,该键是必填" — stopped being true before this card was dispatched. PR #6279 (ADR-0122 phase 2, #6083, merged 2026-08-08 03:47) flipped all 1384 bare aliases to z.input. Measured on this base with tsc:

The ruled remedy is now prohibited. ADR-0122 D3 forbids a permanent synonym, and phase 2 retired 102 XInput aliases for exactly this reason. check:spec-parsed-aliasrule 4 rejects "an XInput that denotes exactly what a bare name denotes". Adding ApiEndpointInput would therefore fail a CI gate — so it is deliberately not added, and the file says why so the next reader does not re-add it.

Not #6606, incidentally: that PR added ApiMapping and four other api/ aliases, and never touched ApiEndpoint.

Item落点BeforeAfter
1packages/spec/src/migrations/registry.tsdeclarative-apis-endpoints-live"an omission is SAFE" with no writable spellingnames ApiEndpoint for declarations, ApiEndpointParsed for parse results
2docs/protocol-upgrade-guide.md, spec-changes.jsonregenerated (gen:upgrade-guide, gen:spec-changes) — never hand-edited
3endpoint.zod.ts docblocksilent on the two statesdocuments the split and why ApiEndpointInput must not come back
4packages/spec/src/api/endpoint.test.tscompile-time pins both ways (ApiEndpoint omits; ApiEndpointParsed requires, via @ts-expect-error)

Verification

Build order: the worktree's first command was pnpm --filter @objectstack/spec build; cross-package work built each closure first (pnpm --filter '@objectstack/metadata^...' build), then a full workspace build before the build-dependent gates and typecheck. Sweep direction for the consumption radius is the prefix form — pnpm --filter '...@objectstack/spec' is downstream consumers — and because this is a contract tightening the full-repo gate is the standard, with scoped filters used only to iterate.

SuiteResult
@objectstack/spec345 files / 8870 tests pass
@objectstack/metadata27 files / 583 tests pass
full workspace buildpass
pnpm lint (ESLint job — hosts the family check:* gates)pass
root check:* × 40 + spec-scoped check:* × 17pass
turbo typecheck (TypeScript Type Check job)pass
CI on this branchconverged: 25 checks, 23 success + 2 skipped, 0 failures

check:authorable-surface is green; gen:schema left authorable-surface.base.json untouched and reports the expected baseRev lag (the anchor is a snapshot proved authentic, not current) — not hand-edited, not re-anchored. packages/spec/json-schema/ is gitignored, so the new additionalProperties: false is regenerated rather than committed.

ADR-0087 disposition (#6148). The changeset answers the question in writing as not-required (no-migration-prescription): this change retires no key, only the posture moves, and an undeclared key was never honoured — so no stored shape carries one and nothing that ever worked becomes invalid. Worth recording how the first two attempts failed, because it is a neat self-reference: the marker's own category name supplies the word "migration" and my two worked examples supplied the arrow, so the disposition line matched the gate's own rewrite-prescription detector and read as contradicting itself. The reasoning is unchanged; the arrow syntax is gone.

Three gates failed on the first pass and all three were infrastructure, not this change: check:i18n, check:i18n-coverage, check:app-nav-i18n each require the built CLI and each said so explicitly ("Nothing was checked … this result says NOTHING"). After the full workspace build, all three pass.

Reverse verification — direction declared before running

Predicted: the new rejection cases go RED with the fix removed; the controls stay GREEN. Taken out with git checkout origin/main -- packages/spec/src/api/endpoint.zod.ts (never git stash — that stack is shared across worktrees).

PackageWith fixFix removed
@objectstack/spec8870 pass12 fail — the 11 new #5384 cases + api REJECTS unknown keys
@objectstack/metadata583 pass9 fail — 7 bookkeeping-key prescriptions + the body-key case + the publish-gate namespace case

Controls held in both directions: the clean endpoint parses, the loader-stamped ADR-0010 envelope parses, and authRequired still defaults to true.

⚠️One honest gap in that table. The two #5227 compile-time pins stayed green in both directions, and that is not a weak test — reverting endpoint.zod.ts cannot exercise them, because the fix they pin landed in PR #6279, not here. They would go red only if someone flipped ApiEndpoint back to z.infer, which is exactly the regression worth pinning.

Blast radius

An apis: entry carrying an undeclared key now fails objectstack validate, objectstack build and the metadata write path instead of publishing with the key discarded. Measured in-tree impact: zero (see the corpus table). Callers were enumerated rather than assumed — the two packages/metadata parse sites, plus the third at selectDocumentableEndpoints (packages/rest/src/openapi-endpoints.ts), which #5309's dev measured as bodies-only and which is re-covered here by the full-repo suite.

Notes for the reviewer


Generated by Claude Code

…e the author state on the upgrade path (#5384, #5227)
`api` became a registered metadata type at #5312, making `endpoint.zod.ts` an
authoring surface (`defineStack({ apis })`, the Studio form, `PUT /meta/api/:name`)
while it was still an open `z.object`. A `cacheTTL` / `objectParam` /
`outputMappings` typo parsed green, published green, and served without the policy
or projection its author wrote.
The shape is `strictObject` now, with curated wrong-layer prescriptions for
`namespace` (ADR-0121 D2 — derived from `manifest.namespace`, never per-endpoint)
and the six stored-envelope bookkeeping keys.
The ORDER is the record: closing this was measured and refused first, because the
same schema parsed STORED rows carrying `packageId` / `state`. #5309 (PR #6576)
paid that debt at the layer that owned it (`peelStoredEnvelope`), after which the
conversion was an ordinary #4001 one. `ApiEndpointSchema` never learned a
bookkeeping key.
Corpus measured before landing: examples/* = 2 endpoints, cloud = 0 endpoints,
zero undeclared keys either side, so direction A carried and no key had to stay.
`api` leaves STILL_STRIP (closed 24 -> 25 of 26; `view` alone remains) and the CLI
gate row moves NOT_YET_CLOSED -> GATED_AT.
#5227 needed no schema change: ADR-0122 phase 2 (PR #6279) already made
`ApiEndpoint` the author state, so omitting `authRequired` compiles. The
`declarative-apis-endpoints-live` upgrade-guide entry now says so with a usable
annotation. An `ApiEndpointInput` alias is deliberately NOT added — after the flip
it would be a synonym, which ADR-0122 D3 forbids and `check:spec-parsed-alias`
rule 4 rejects.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011sGk4SKHqGRgmmqUok1P8M
… guidance
Caught by the drift pin added in the same change, on its first run:
`STORED_ENVELOPE_KEYS` has SEVEN entries and the guidance table listed six.
This is the drift the pin exists for, and it is silent in the direction that
matters — `package` is peeled correctly on the stored paths, so every other test
stays green, while an author who hand-writes it into a declaration gets the
generic "unrecognized key" instead of the wrong-layer prescription. A rejection
arriving without its upgrade is the ledger's finding 7.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011sGk4SKHqGRgmmqUok1P8M
@vercel

vercelBot commented Aug 9, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 9, 2026 12:15am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

112 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/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @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/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @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 @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @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/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • 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/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 @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @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/spec)
  • content/docs/plugins/packages.mdx(via @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/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.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/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/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @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/apps.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/field-grouping-and-order.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.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 9, 2026
…#6148)
The gate does not decide whether an entry is needed; it requires the question
be answered. Answered `not-required (no-migration-prescription)`: this change
retires no key, only the unknown-key posture moves, and an undeclared key was
never honoured — so no stored shape carries it and nothing that ever worked
becomes invalid. The schema rejection is the upgrade channel and is more
specific than a ledger line could be.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011sGk4SKHqGRgmmqUok1P8M
…w syntax
The marker's own category name supplies the word 'migration', and the two
worked examples supplied the arrow — so the disposition line matched the
gate's framed-line rewrite-prescription detector and contradicted itself.
Same reasoning, stated without FROM/TO arrow syntax; the per-key prescriptions
live in the schema's aliases/guidance, which is where an author meets them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011sGk4SKHqGRgmmqUok1P8M
@os-zhuang
os-zhuang marked this pull request as ready for review August 9, 2026 01:07
@os-zhuang
os-zhuang added this pull request to the merge queueAug 9, 2026
Merged via the queue into main with commit eaed61fAug 9, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5384-api-endpoint-strict branch August 9, 2026 01:50
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-zhuang@claude