Repository navigation
fix(spec)!: defineConnector() refuses a top-level key the connector does not declare - #28815
Conversation
defineConnector() parsed the open ConnectorSchema, so an undeclared top-level key was dropped before the closed entry behind defineStack could see it. It now parses the closed carrier behind the same retired-default residue stage, and refuses the key by name with the entry's message, aliases and guidance. ConnectorSchema keeps its open posture for the defs plugins hand to registerConnector. Claude-Session: https://claude.ai/code/session_01ADHkmT9tAeotFczB7jYquW Co-authored-by: Claude <noreply@anthropic.com>
… ledger and changesets Claude-Session: https://claude.ai/code/session_01ADHkmT9tAeotFczB7jYquW Co-authored-by: Claude <noreply@anthropic.com>
…ctor shape's docblock Claude-Session: https://claude.ai/code/session_01ADHkmT9tAeotFczB7jYquW Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 468b9519489a18f417ec41daed57e4f4fe2c6296 && git checkout 468b9519489a18f417ec41daed57e4f4fe2c6296
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 70bf43438509ca57a2b18c35c77c735f2ec57beb bb3b9bc4d400421a7d6f81b1160c1de483bad2eb && git checkout -B drift-repro 70bf43438509ca57a2b18c35c77c735f2ec57beb && git merge --no-ff bb3b9bc4d400421a7d6f81b1160c1de483bad2eb
node scripts/docs-audit/affected-docs.mjs --json 70bf43438509ca57a2b18c35c77c735f2ec57beb
|
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Head-sha: 维护者速读
席位意见:可落地,等契约复审。实现只换了解析用的 schema,签名和返回类型不变,没新增导出;选闭合基形而非完整的 stack 条目 schema 是对的,后者会连带拒收插件定义上的内联 席位核对 — ACCEPT ·
|
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Taken on the lane's Generated by Claude Code |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract reviewServed-tier: Face: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Findings:
Implemented-by: VERDICT: PASS Generated by Claude Code |
Requested by Steve · project thread
Fixes #28738
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — the published skills' connector recipes (
skills/objectstack-automation/references/integration-recipes.md) author declared keys only and say nothing about the helper's key posture, so no skill text turns false.维护者速读
defineConnector()改为解析闭合的连接器形状;写了未声明的顶层键会当场报错并点名,附处方(凭据类键指向auth.credentialRef,提供方设置指向providerConfig,timeout指向requestTimeoutMs)。ConnectorSchema,先把未声明的键悄悄丢掉,defineStack等闭合门再也看不到,作者放错位置的设置(比如把timeout写在根上)无声丢失。现在它和其它所有连接器入口一样响亮拒收。defineConnector里写了未声明键,加载时会报错。插件给registerConnector的定义不变。回滚只需把defineConnector函数体改回ConnectorSchema.parse。packages/spec/src/integration/connector.zod.ts的DefineConnectorSchema与defineConnector函数体;connector-entry-undeclared-keys.test.ts新增的一组测试;.changeset/harden-connector-undeclared-keys.md改动的那一句。pnpm --filter @objectstack/spec exec vitest run src/integration/connector-entry-undeclared-keys.test.ts,应看到 24 条全过。What changes
defineConnector()(@objectstack/spec,@objectstack/spec/integration) parses a module-privateDefineConnectorSchema: the closed carrierDeclarativeConnectorEntryBaseSchemabehind the same retired-default residue stage (CONNECTOR_RETIRED_KEY_RESIDUE) thatConnectorSchemaandDeclarativeConnectorEntrySchemause. An undeclared top-level key now throws aZodErrorwith oneunrecognized_keysissue at the root, carrying the entry's own message, aliases and guidance. The signature is unchanged: inputz.inputofConnectorSchema, returnConnectorParsed;check:api-surfaceand the rest ofcheck:generatedneeded no regeneration.ConnectorSchema(the def a plugin hands toregisterConnector) keeps its open posture; option C of the card is not taken.DeclarativeConnectorEntrySchema(no inlineauthentication,providerConfig/authneed aprovider, noactionson a provider-bound instance, the oauth2 / signed instance rules). Every stack door still applies them to the value the helper returns. See the Acceptance notes for why, and the measured fact that the two choices agree on every in-repo caller.connector-entry-undeclared-key-refused(landed with feat(spec)!: an authored connector entry refuses a top-level key it does not declare; the connector read withholds URL credential parts and a non-object auth #28688, not yet released) now namesdefineConnector()in its surface and reason. This PR's changeset disposes asnot-required (already-registered connector-entry-undeclared-key-refused).connector.zod.tsand theintegration/row ofdocs/audits/2026-07-unknown-key-strictness-ledger.mdstop saying the helper parses the open shape.A pending release note this PR corrects
.changeset/harden-connector-undeclared-keys.md(from #28688, still pending) said: "ConnectorSchema— the def a plugin hands toregisterConnector, and the shapedefineConnector()parses — keeps its posture." This PR makes thedefineConnector()half false, so the clause is removed and the sentence now reads "ConnectorSchema— the def a plugin hands toregisterConnector— keeps its posture."check-empty-changesetrefuses this edit by design (the DELIBERATE CORRECTION class), so Check Changeset stays red until a maintainer confirms the correction here in writing. It is not a required context.Tests
New
describeblock inpackages/spec/src/integration/connector-entry-undeclared-keys.test.ts(12 cases; the file had 12): for an instance and a descriptor, each refusal pinned bycode,pathandkeyson the thrownZodError, the value never echoed;timeout→requestTimeoutMswith the declared spelling as control; the credentialRef andproviderConfigguidance; the helper's issue equal to the stack door's issue on the same entry; residue (status: 'inactive') and the ADR-0010 envelope still accepted; and a pin that the helper applies no stack-only refinement. Forced test edits: none; no existing test changed.Ablation at 4185e6a (
node scripts/ablation-replace.mjs, body put back toConnectorSchema.parse(config), anchor 1 → 0, restored blob equal to HEAD): 7 failed | 17 passed (24). The 7 are exactly the refusal cases; controls, residue, envelope and the no-refinement pin pass on both sides, as they should.Readings (packages/spec and the consumers that import defineConnector)
@objectstack/spectest at f668513: Test Files 788 passed (788), Tests 23279 passed, 1 todo. Typecheck (incl.check:test-typecheck): exit 0.@objectstack/specsrc/integration/at bb3b9bc: Test Files 14 passed (14), Tests 264 passed.check:generatedat bb3b9bc: all 14 artifacts up to date.git grep "defineConnector(", closure built first (turbo run build, 65 tasks), at f668513:@objectstack/example-showcase(4 calls): typecheck exit 0; Test Files 42 passed (42), Tests 511 passed.@objectstack/connector-app-github(1 call): Test Files 2 passed (2), Tests 13 passed; typecheck exit 0.@objectstack/connector-app-stripe(1 call): Test Files 2 passed (2), Tests 19 passed; typecheck exit 0.@objectstack/downstream-contract(source-resolution identity pin on both import paths): Test Files 3 passed (3), Tests 31 passed; typecheck exit 0.pnpm --filter @objectstack/dogfood test, no shard, at f668513): VERDICT command-exit 0; Test Files 377 passed, 3 skipped (380); Tests 3101 passed, 28 skipped. One named skip isprint-service-render(no built Console on this host).packages/spec/distat bb3b9bc equals the dist the dogfood run read (f668513): 360 of 360 non-map files byte-identical once esbuild's chunk-hash names are normalized. So the dogfood reading carries to the final head.Acceptance notes
DeclarativeConnectorEntrySchemawould also refuse an inline non-noneauthenticationand the other stack-only rules at the helper. That would break any plugin code that builds aregisterConnectordef throughdefineConnector, which is the code-to-code break option C was kept out of this card for. Measured: all 7 in-repo callers and all 5os:checkblocks incontent/docs/automation/connectors.mdxgive the same verdict under either choice, so the choice changes nothing in this repo. It is listed as an open question for the seat.strictObjectcarrier. It says "this connector entry", which fits the documented use.content/docs/releases/v17/17-6.mdxnamesdefineConnectoramong the doors that refuse retired keys. That is release-owned and still true; not touched.Verification
Gate list derived by
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(84 commands) and run at the final head bb3b9bc; reconciled with--ran(exit codes recorded): 84 derived, 84 run, 0 NOT-MEASURED, 0 UNRUN. 83 exit 0. One is red by design:node scripts/check-empty-changeset.mjs --base origin/mainexit 1, the DELIBERATE CORRECTION class above.Gate notes
check-plugin-teardown-shape.mjs --self-testneeded its pinned fixture commit in the shallow clone (fetched; exit 0, 48 cases), andpnpm check:dual-build-cjs-loadsneeded 7 packages built (built; exit 0, 115 require entry points across 70 packages load).check-adr-0087-registration.mjs --base origin/main: exit 0. The new changeset reads[BREAKING+bang+clause-②-narrowing+metadata-protocol-narrowed], dispositionnot-required (already-registered).pnpm check:dev-prereqs: exit 1. It reads this host's build state (35ui/packages with no dist), not this diff. Reported, not acted on.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses: eslint on the changed files exits 0 locally; the repo-wide run is CI's.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed (none were stale).content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code