Skip to content

Commit f985b3f

Browse files
os-zhuangclaude
andauthored
fix(spec,core,cloud-connection,metadata): HTTP 契约归一 —— 死影子副本删除,http.server 定为 canonical 并入账 ledger (#4251) (#4393)
* fix(spec,core,cloud-connection,metadata): one HTTP contract, one canonical slot name (#4251) packages/core/src/contracts/ was a dead near-copy of the real contracts -- zero importers (no relative import, no subpath export, not a tsup entry; core's barrel has re-exported the spec versions all along) -- and it had already DIVERGED (spec's IHttpResponse grew write?/end?, IHttpRequest grew rawBody?; the copy never did). Anyone who grepped into it read a stale contract nothing enforces -- the both-humans-and-AI failure mode behind the false http.server exemption (#4382). Deleted; zero-risk by construction. `http.server` is the canonical slot name and the ledger now says so: ServiceSlotContracts gains 'http.server': IHttpServer plus the deprecated 'http-server' alias entry (same instance -- hono-plugin, qa node-plugin and cloud's two server entrypoints all register both, alias commented "backward compatibility"). Canonical is the only name on EVERY provider path -- runtime's config.server path registers no alias, so the three cloud-connection plugins reading the alias alone found an empty slot there. All readers now go canonical-first with the alias as a fallback that dies with the alias registrations; registrations untouched this release, both sites carry the deprecation note. getRawApp?(): any joins IHttpServer -- the deliberate framework-handle escape, declared once with the rationale; four consumers declared it locally before (cloud-connection x2, metadata HMR, cloud node-server), and the local RawAppHost / HttpServerWithRawApp types are deleted. IMetadataService.bulkRegister/bulkUnregister declare the write options their implementation always accepted (bulkRegister's contract dropped the MetadataWriteOptions half it intersects in; bulkUnregister declared no options at all). Same shape as B2's IDataEngine read-methods gap. Baseline 168 -> 167 (marketplace-install-local's lookup typed while touched). Ledger test pins both slot entries and the alias equality. Verified: spec build (dts) + 7192 tests / 281 files; core, metadata, cloud-connection, plugin-hono-server dts builds; runtime 1001/69, rest 539/36, plugin-auth 579/26, plugin-sharing 226/11, service-settings 196/14, metadata 281/13, hono-server 135/12, http-conformance 46/2; ratchet holds 167/36 none new; eslint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cloud-connection,metadata): the canonical-first read needs a per-name try -- getService throws on an empty slot (#4251) CI caught the shape my local run could not (I built cloud-connection but ran only metadata's tests -- cloud-connection's own suites mock a kernel that registers ONLY the alias): `getService` THROWS for an unregistered slot, so try { a = getService('http.server') ?? getService('http-server'); } catch {...} never reaches the alias -- the first name's throw exits the whole try. Split into a per-name try (readServer helper) in all four readers. Worth recording: the pre-existing alias-first read in metadata/plugin.ts had the SAME shape, so its `?? getService('http.server')` fallback never once fired either -- a decorative fallback, the declared-vs-actual gap this work line keeps finding, now actually implemented in both directions. Verified serially (the earlier 6-file FAIL was local vitest concurrency noise while spec's dts build ran in parallel -- single-file and serial reruns green): cloud-connection 64/12, metadata 281/13, both dts builds, eslint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 8b50cb3 commit f985b3f

15 files changed

Lines changed: 183 additions & 346 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/core": patch
4+
"@objectstack/cloud-connection": patch
5+
"@objectstack/metadata": patch
6+
"@objectstack/plugin-hono-server": patch
7+
---
8+
9+
fix(spec,core,cloud-connection,metadata): one HTTP contract, one canonical slot name — and the dead shadow copy that helped cause the false exemption is deleted (#4251)
10+
11+
**`packages/core/src/contracts/` was a dead near-copy of the real contracts,
12+
and it is gone.** The directory (http-server.ts, data-engine.ts, logger.ts) had
13+
ZERO importers — no relative import, no subpath export, not a tsup entry;
14+
core's barrel has re-exported the `@objectstack/spec/contracts` versions all
15+
along ("Re-export contracts from @objectstack/spec for backward
16+
compatibility"). But the shadow had already **diverged** from the live
17+
contract (spec's `IHttpResponse` grew `write?`/`end?` and `IHttpRequest` grew
18+
`rawBody?`; the copy never did), so anyone who grepped their way into it read a
19+
stale contract that nothing enforces — the exact both-humans-and-AI failure
20+
mode behind the false `http.server` exemption (#4382). Deleting it is
21+
zero-risk by construction: nothing could reach it.
22+
23+
**`http.server` is the canonical slot name, and the ledger now says so.**
24+
`ServiceSlotContracts` gains `'http.server': IHttpServer` plus the deprecated
25+
`'http-server'` alias entry (same instance — hono-plugin and qa's node-plugin
26+
register both two lines apart; cloud's two server entrypoints do the same).
27+
Canonical is the only name present on EVERY provider path: runtime's
28+
`config.server` path registers no alias, so the three cloud-connection plugins
29+
that read the alias alone (marketplace-proxy, runtime-config,
30+
marketplace-install-local) found an empty slot there — a live miss, now fixed:
31+
all readers go canonical-first with the alias as a fallback that dies with the
32+
alias registrations. The registrations themselves are untouched this release;
33+
both sites now carry the deprecation note.
34+
35+
**`getRawApp?(): any` joins `IHttpServer`** — the deliberate framework-handle
36+
escape, declared once. Four consumers were each declaring it locally
37+
(cloud-connection ×2, metadata's HMR routes, cloud's serverless node-server);
38+
those local `RawAppHost`/`HttpServerWithRawApp` types are deleted. The `any`
39+
return is deliberate and documented at the single declaration: the handle's
40+
real type belongs to the framework, and naming it would give the contract a
41+
framework dependency. Adapters are not required to expose it; consumers
42+
feature-detect.
43+
44+
**`IMetadataService.bulkRegister`/`bulkUnregister` declare the write options
45+
their implementation has always accepted.** `bulkRegister`'s contract options
46+
dropped the `MetadataWriteOptions` half its implementation intersects in
47+
(`notify` is destructured on the method's first line); `bulkUnregister`
48+
declared no options at all while the manager takes them. Same shape as the
49+
`IDataEngine` read-methods gap from B2: a caller typed to the contract could
50+
not reach the channel without erasing the lookup. Both additive; no implementor
51+
or caller breaks.
52+
53+
Slot-lookup baseline ratchets 168 → 167 (marketplace-install-local's lookup
54+
typed while touched).

‎packages/cloud-connection/src/marketplace-install-local-plugin.ts‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { resolveMarketplacePublicBaseUrl } from './marketplace-public-url.js';
4848
import { LocalManifestSource, type InstalledManifestEntry } from './local-manifest-source.js';
4949
import { ConnectionCredentialStore } from './connection-credential-store.js';
5050
import { MARKETPLACE_INSTALLED_UI_BUNDLE } from './marketplace-ui.js';
51+
import type { IHttpServer } from '@objectstack/spec/contracts';
5152

5253
const ROUTE_BASE = '/api/v1/marketplace/install-local';
5354

@@ -120,14 +121,20 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
120121
await this.rehydrate(ctx);
121122

122123
// 2. Mount HTTP endpoints.
123-
let httpServer: any;
124-
try {
125-
httpServer = ctx.getService('http-server');
126-
} catch {
124+
// [#4251] Read canonical-first with a REAL per-name fallback:
125+
// `getService` THROWS for an empty slot, so a single try around
126+
// `canonical ?? alias` never reaches the alias — the shape the old
127+
// alias-first read had too, meaning its fallback never once fired.
128+
const readServer = (name: string): IHttpServer | undefined => {
129+
try { return ctx.getService<IHttpServer>(name); } catch { return undefined; }
130+
};
131+
// Canonical first — see marketplace-proxy-plugin.
132+
const httpServer = readServer('http.server') ?? readServer('http-server');
133+
if (!httpServer) {
127134
ctx.logger?.warn?.('[MarketplaceInstallLocal] http-server not available — install endpoints not mounted');
128135
return;
129136
}
130-
if (!httpServer || typeof httpServer.getRawApp !== 'function') {
137+
if (typeof httpServer.getRawApp !== 'function') {
131138
ctx.logger?.warn?.('[MarketplaceInstallLocal] http-server missing getRawApp() — install endpoints not mounted');
132139
return;
133140
}

‎packages/cloud-connection/src/marketplace-proxy-plugin.ts‎

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,6 @@ import {
3535

3636
import type { IHttpServer } from '@objectstack/spec/contracts';
3737

38-
/**
39-
* The `http-server` slot plus the one member this plugin needs from it.
40-
*
41-
* [#4251] `IHttpServer` is a real contract (`@objectstack/spec/contracts`) and
42-
* this slot is bound to it — but `getRawApp()` is NOT on it, deliberately: the
43-
* contract is framework-agnostic and the raw app is the framework's own handle
44-
* (Hono here). So the escape is one named member returning `any`, scoped to the
45-
* one thing that genuinely cannot be typed framework-agnostically, instead of
46-
* the whole lookup collapsing to `any`. The probe below is what runs when a
47-
* host serves the slot with an adapter that exposes no raw app.
48-
*/
49-
type RawAppHost = IHttpServer & { getRawApp?(): any };
50-
5138
const MARKETPLACE_PREFIX = '/api/v1/marketplace';
5239

5340
/**
@@ -196,14 +183,21 @@ export class MarketplaceProxyPlugin implements Plugin {
196183
manifest?.register?.(MARKETPLACE_BROWSE_UI_BUNDLE);
197184
} catch { /* no manifest service */ }
198185

199-
let httpServer: RawAppHost | undefined;
200-
try {
201-
httpServer = ctx.getService<RawAppHost>('http-server');
202-
} catch {
186+
// [#4251] Read canonical-first with a REAL per-name fallback:
187+
// `getService` THROWS for an empty slot, so a single try around
188+
// `canonical ?? alias` never reaches the alias — the shape the old
189+
// alias-first read had too, meaning its fallback never once fired.
190+
const readServer = (name: string): IHttpServer | undefined => {
191+
try { return ctx.getService<IHttpServer>(name); } catch { return undefined; }
192+
};
193+
// `http.server` is canonical (the only name every provider
194+
// registers); the alias fallback dies with the alias registrations.
195+
const httpServer = readServer('http.server') ?? readServer('http-server');
196+
if (!httpServer) {
203197
ctx.logger?.warn?.('[MarketplaceProxyPlugin] http-server not available — marketplace routes not mounted');
204198
return;
205199
}
206-
if (!httpServer || typeof httpServer.getRawApp !== 'function') {
200+
if (typeof httpServer.getRawApp !== 'function') {
207201
ctx.logger?.warn?.('[MarketplaceProxyPlugin] http-server missing getRawApp() — marketplace routes not mounted');
208202
return;
209203
}

‎packages/cloud-connection/src/runtime-config-plugin.ts‎

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,6 @@ import type { Plugin, PluginContext } from '@objectstack/core';
4343
import { resolveCloudUrl } from './cloud-url.js';
4444
import type { IHttpServer } from '@objectstack/spec/contracts';
4545

46-
/**
47-
* The `http-server` slot plus the one member this plugin needs from it.
48-
*
49-
* [#4251] `IHttpServer` is a real contract (`@objectstack/spec/contracts`) and
50-
* this slot is bound to it — but `getRawApp()` is NOT on it, deliberately: the
51-
* contract is framework-agnostic and the raw app is the framework's own handle
52-
* (Hono here). So the escape is one named member returning `any`, scoped to the
53-
* one thing that genuinely cannot be typed framework-agnostically, instead of
54-
* the whole lookup collapsing to `any`. The probe below is what runs when a
55-
* host serves the slot with an adapter that exposes no raw app.
56-
*/
57-
type RawAppHost = IHttpServer & { getRawApp?(): any };
58-
5946
/**
6047
* The `env-registry` slot's hostname resolvers — see the call site for both
6148
* spellings. Async by contract: the consumer awaits the result, and declaring
@@ -196,14 +183,20 @@ export class RuntimeConfigPlugin implements Plugin {
196183

197184
start = async (ctx: PluginContext): Promise<void> => {
198185
ctx.hook('kernel:ready', async () => {
199-
let httpServer: RawAppHost | undefined;
200-
try {
201-
httpServer = ctx.getService<RawAppHost>('http-server');
202-
} catch {
186+
// [#4251] Read canonical-first with a REAL per-name fallback:
187+
// `getService` THROWS for an empty slot, so a single try around
188+
// `canonical ?? alias` never reaches the alias — the shape the old
189+
// alias-first read had too, meaning its fallback never once fired.
190+
const readServer = (name: string): IHttpServer | undefined => {
191+
try { return ctx.getService<IHttpServer>(name); } catch { return undefined; }
192+
};
193+
// Canonical first — see marketplace-proxy-plugin.
194+
const httpServer = readServer('http.server') ?? readServer('http-server');
195+
if (!httpServer) {
203196
ctx.logger?.warn?.('[RuntimeConfigPlugin] http-server not available — runtime/config not mounted');
204197
return;
205198
}
206-
if (!httpServer || typeof httpServer.getRawApp !== 'function') {
199+
if (typeof httpServer.getRawApp !== 'function') {
207200
ctx.logger?.warn?.('[RuntimeConfigPlugin] http-server missing getRawApp() — runtime/config not mounted');
208201
return;
209202
}

‎packages/core/src/contracts/data-engine.ts‎

Lines changed: 0 additions & 62 deletions
This file was deleted.

‎packages/core/src/contracts/http-server.ts‎

Lines changed: 0 additions & 151 deletions
This file was deleted.

0 commit comments

Comments
 (0)