Skip to content

Commit 10575f3

Browse files
os-zhuangclaude
andauthored
fix(lint,metadata): revoke the http.server lint exemption (#4251) (#4382)
#4321 put `http.server` in UNCONTRACTED_SLOTS on the ground that "no IHttpServer contract exists". The contract does exist (packages/spec/src/contracts/http-server.ts) and eight call sites were already resolving the slot as getService<IHttpServer>(...) when the exemption was written. An exemption is a claim like any other, and this one rested on a premise nobody checked -- the same shape as the gaps the rule exists to find. Revoked. That surfaced 9 erasures the exemption had been hiding: 7 in files never grandfathered, 2 as count growth inside grandfathered ones, none of which the baseline could legally absorb. All typed to IHttpServer; packages/metadata/src/plugin.ts came out clean entirely, so the baseline ratchets DOWN to 168 sites in 36 files and loses a file. Confirmed and reported, not changed: `http.server` and `http-server` are the SAME instance under two names (plugin-hono-server and qa/node-plugin each register it twice, two lines apart; runtime's config.server path registers only the dotted one) -- removing a registration is a runtime change and belongs with whoever picks the canonical name. And `IHttpServer` is defined TWICE and has already diverged: spec's (15 importers) declares write?()/end?(), core's (8 importers) does not. Spec's is the superset and the one the ledger points at; core's is a stale near-copy that should re-export it, in its own change. Verified: ratchet holds (168/36, none new, no files added vs d92c72d); eslint clean; metadata dts build succeeds; runtime 995/68, client 204/15, metadata 281/13, http-conformance 46/2. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 78caf51 commit 10575f3

12 files changed

Lines changed: 87 additions & 24 deletions
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
"@objectstack/metadata": patch
3+
---
4+
5+
fix(lint,metadata): revoke the `http.server` lint exemption — its stated reason was false (#4251)
6+
7+
`http.server` was added to `UNCONTRACTED_SLOTS` in #4321 on the ground that
8+
"no IHttpServer contract exists". The contract does exist —
9+
`packages/spec/src/contracts/http-server.ts` — and eight call sites were
10+
already resolving the slot as `getService<IHttpServer>(…)` when the exemption
11+
was written. An exemption is a claim like any other, and this one rested on a
12+
premise nobody checked: the same shape as the gaps the rule exists to find.
13+
14+
Revoked. That surfaced **9 erasures the exemption had been hiding** — 7 in
15+
files never grandfathered, 2 as count growth inside grandfathered ones, none of
16+
which the baseline could legally absorb. All typed to `IHttpServer`;
17+
`packages/metadata/src/plugin.ts` came out clean entirely, so the baseline
18+
ratchets **DOWN to 168 sites in 36 files** and loses a file.
19+
20+
Two things confirmed on the way, reported rather than changed:
21+
22+
**`http.server` and `http-server` are the same instance under two names.**
23+
plugin-hono-server and qa's node-plugin each register it twice, two lines
24+
apart; runtime's `config.server` path registers only `http.server`.
25+
`metadata/src/plugin.ts` reads both with a `??`, which is how it survived. No
26+
registration is removed here — that is a runtime-behaviour change and belongs
27+
with whoever picks the canonical name.
28+
29+
**`IHttpServer` is defined twice and the two have already diverged.**
30+
`packages/spec/src/contracts/http-server.ts` (15 importers) declares `write?()`
31+
and `end?()`; `packages/core/src/contracts/http-server.ts` (8 importers) does
32+
not. Spec's is the superset and the one the ledger points at, so it is the
33+
source; core's is a stale near-copy and should re-export it. Left for its own
34+
change — collapsing a duplicated contract is not a lint fix.
35+
36+
Also worth a note for whoever writes the wider HTTP contract: `getRawApp()` now
37+
has a **third** independent consumer (metadata's HMR routes, joining
38+
cloud-connection's two). It is deliberately absent from `IHttpServer` — the
39+
contract is framework-agnostic and the raw app is the framework's own handle —
40+
so each consumer names it locally. Three is enough evidence to decide whether
41+
that stays the right answer.

eslint.config.mjs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,16 @@ const SLOT_LOOKUPS = ['resolveService', 'getService', 'getRequestKernelService']
6969
// place, not sprinkled through the code. Deleting a name from this list is how
7070
// the exemption ends once that contract gets written.
7171
//
72-
// Entries are spliced into a regex, so escape metacharacters (`http\\.server`).
73-
// `http.server` is served by three providers (plugin-hono-server, runtime's
74-
// config.server path, qa's node-plugin) and no IHttpServer contract exists;
75-
// callers read only `getPort()`.
76-
const UNCONTRACTED_SLOTS = ['protocol', 'mcp', 'kernel-resolver', 'scope-manager', 'http\\.server'].join('|');
72+
// Entries are spliced into a regex, so escape metacharacters if one returns.
73+
//
74+
// [#4251] `http.server` was here, on the stated ground that "no IHttpServer
75+
// contract exists". That was FALSE when it was written: the contract is
76+
// `packages/spec/src/contracts/http-server.ts`, and eight call sites were
77+
// already resolving the slot as `getService<IHttpServer>(…)`. An exemption is a
78+
// claim like any other, and this one rested on a premise nobody checked — the
79+
// same shape as the gaps this rule exists to find. Revoked; the slot is
80+
// accounted for like every other contracted slot.
81+
const UNCONTRACTED_SLOTS = ['protocol', 'mcp', 'kernel-resolver', 'scope-manager'].join('|');
7782

7883
// Exported so `scripts/check-slot-lookup-ratchet.mjs` can identify THIS rule's
7984
// reports among the other `no-restricted-syntax` rules, by exact message —

packages/client/src/client.batch-transaction.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
3333
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
3434
import { createRestApiPlugin } from '@objectstack/runtime';
3535
import { ObjectStackClient } from './index';
36+
import type { IHttpServer } from '@objectstack/spec/contracts';
3637

3738
describe('data.batchTransaction (live Hono, #1604)', () => {
3839
let baseUrl: string;
@@ -103,8 +104,8 @@ describe('data.batchTransaction (live Hono, #1604)', () => {
103104
await ql.syncObjectSchema('project');
104105
await ql.syncObjectSchema('task');
105106

106-
const httpServer = kernel.getService<any>('http.server');
107-
baseUrl = `http://localhost:${httpServer.getPort()}`;
107+
const httpServer = kernel.getService<IHttpServer>('http.server');
108+
baseUrl = `http://localhost:${httpServer.getPort!()}`;
108109
client = new ObjectStackClient({ baseUrl });
109110
}, 30_000);
110111

packages/client/src/client.environment-scoping.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
2020
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
2121
import { createRestApiPlugin } from '@objectstack/runtime';
2222
import { ObjectStackClient } from './index';
23+
import type { IHttpServer } from '@objectstack/spec/contracts';
2324

2425
describe('Project-scoped REST routing (live Hono)', () => {
2526
let baseUrl: string;
@@ -82,8 +83,8 @@ describe('Project-scoped REST routing (live Hono)', () => {
8283
// write fails with `no such table`.
8384
await ql.syncObjectSchema('task');
8485

85-
const httpServer = kernel.getService<any>('http.server');
86-
const port = httpServer.getPort();
86+
const httpServer = kernel.getService<IHttpServer>('http.server');
87+
const port = httpServer.getPort!();
8788
baseUrl = `http://localhost:${port}`;
8889
}, 30_000);
8990

packages/client/src/client.hono.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
55
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
66
import { createRestApiPlugin } from '@objectstack/runtime';
77
import { ObjectStackClient } from './index';
8+
import type { IHttpServer } from '@objectstack/spec/contracts';
89

910
describe('ObjectStackClient (with Hono Server)', () => {
1011
let baseUrl: string;
@@ -130,8 +131,8 @@ describe('ObjectStackClient (with Hono Server)', () => {
130131
await ql.syncObjectSchema('customer');
131132

132133
// 5. Get Port from Service
133-
const httpServer = kernel.getService<any>('http.server');
134-
const port = httpServer.getPort();
134+
const httpServer = kernel.getService<IHttpServer>('http.server');
135+
const port = httpServer.getPort!();
135136
baseUrl = `http://localhost:${port}`;
136137

137138
console.log(`Test server running at ${baseUrl}`);

packages/metadata/src/plugin.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ const ARTIFACT_FIELD_TO_TYPE: Record<string, string> = {
109109
// callers below and for `view-expand.test.ts`.
110110
export { isAggregatedViewContainer, expandViewContainer } from '@objectstack/spec';
111111
import { isAggregatedViewContainer, expandViewContainer } from '@objectstack/spec';
112+
import type { IHttpServer } from '@objectstack/spec/contracts';
113+
114+
/** `IHttpServer` plus the framework-specific raw-app handle this route needs. */
115+
type HttpServerWithRawApp = IHttpServer & { getRawApp?(): any };
112116

113117
export interface MetadataPluginOptions {
114118
rootDir?: string;
@@ -405,8 +409,14 @@ export class MetadataPlugin implements Plugin {
405409
// Production deployments simply won't have a CLI POSTing to this
406410
// endpoint and won't surface the route to clients.
407411
try {
408-
const httpServer = ctx.getService<any>('http-server')
409-
?? ctx.getService<any>('http.server');
412+
// [#4251] Both names are the SAME instance — plugin-hono-server and
413+
// qa/node-plugin each register it twice, two lines apart. Typed to
414+
// the contract now that the `http.server` exemption is revoked;
415+
// `getRawApp()` is not on `IHttpServer` (framework-agnostic by
416+
// design, the raw app is the framework's own handle), so that one
417+
// member is named here — the third consumer to need it.
418+
const httpServer = ctx.getService<HttpServerWithRawApp>('http-server')
419+
?? ctx.getService<HttpServerWithRawApp>('http.server');
410420
if (httpServer && typeof httpServer.getRawApp === 'function') {
411421
const { registerMetadataHmrRoutes } = await import('./routes/hmr-routes.js');
412422
const hub = registerMetadataHmrRoutes(httpServer.getRawApp(), this.manager);

packages/qa/http-conformance/src/conformance.integration.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
2222
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
2323
import { createDispatcherPlugin, createRestApiPlugin } from '@objectstack/runtime';
2424
import { NodeServerPlugin } from './node-plugin.js';
25+
import type { IHttpServer } from '@objectstack/spec/contracts';
2526

2627
type AdapterCase = {
2728
label: 'node' | 'hono';
@@ -97,8 +98,8 @@ async function bootStack(makePlugin: () => any, opts: { withAnalytics?: boolean
9798
// a 404 OBJECT_NOT_FOUND — a routing-shaped symptom for a DDL-shaped cause.
9899
await ql.syncObjectSchema('task');
99100

100-
const httpServer = kernel.getService<any>('http.server');
101-
return { kernel, base: `http://127.0.0.1:${httpServer.getPort()}` };
101+
const httpServer = kernel.getService<IHttpServer>('http.server');
102+
return { kernel, base: `http://127.0.0.1:${httpServer.getPort!()}` };
102103
}
103104

104105
describe.each(ADAPTERS)('IHttpServer conformance on $label adapter', ({ makePlugin }) => {

packages/runtime/src/dispatcher-plugin.ready.integration.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { LiteKernel } from '@objectstack/core';
55
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
66

77
import { createDispatcherPlugin } from './dispatcher-plugin.js';
8+
import type { IHttpServer } from '@objectstack/spec/contracts';
89

910
/**
1011
* Integration regression for framework #2217 seam #2.
@@ -29,8 +30,8 @@ describe('GET /ready over a real HTTP server (integration)', () => {
2930

3031
await kernel.bootstrap();
3132

32-
const httpServer = kernel.getService<any>('http.server');
33-
baseUrl = `http://127.0.0.1:${httpServer.getPort()}`;
33+
const httpServer = kernel.getService<IHttpServer>('http.server');
34+
baseUrl = `http://127.0.0.1:${httpServer.getPort!()}`;
3435
}, 30_000);
3536

3637
afterAll(async () => {

packages/runtime/src/notifications.hono.integration.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { MessagingServicePlugin, MessagingService } from '@objectstack/service-m
1010

1111
import { createDispatcherPlugin } from './dispatcher-plugin.js';
1212
import { DriverPlugin } from './driver-plugin.js';
13+
import type { IHttpServer } from '@objectstack/spec/contracts';
1314

1415
/**
1516
* End-to-end regression for framework #3362 (`#3354 not effective on hono`).
@@ -89,8 +90,8 @@ describe('in-app notifications over a real hono server (integration, #3362)', ()
8990

9091
await kernel.bootstrap();
9192

92-
const httpServer = kernel.getService<any>('http.server');
93-
baseUrl = `http://127.0.0.1:${httpServer.getPort()}`;
93+
const httpServer = kernel.getService<IHttpServer>('http.server');
94+
baseUrl = `http://127.0.0.1:${httpServer.getPort!()}`;
9495
messaging = kernel.getService<MessagingService>('notification');
9596
}, 30_000);
9697

packages/runtime/src/route-parity.integration.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
66
import { isMcpServerEnabled } from '@objectstack/types';
77

88
import { createDispatcherPlugin } from './dispatcher-plugin.js';
9+
import type { IHttpServer } from '@objectstack/spec/contracts';
910

1011
/**
1112
* The statuses that mean "advertised but not actually served" — 404 (route not
@@ -126,8 +127,8 @@ async function bootServe(stubOpts: { notification?: boolean; mcp?: boolean } = {
126127
kernel.use(new HonoServerPlugin({ port: 0, cors: false }));
127128
kernel.use(createDispatcherPlugin({ prefix: '/api/v1', securityHeaders: false, requireAuth: true }));
128129
await kernel.bootstrap();
129-
const httpServer = kernel.getService<any>('http.server');
130-
const baseUrl = `http://127.0.0.1:${httpServer.getPort()}`;
130+
const httpServer = kernel.getService<IHttpServer>('http.server');
131+
const baseUrl = `http://127.0.0.1:${httpServer.getPort!()}`;
131132
return { kernel, baseUrl };
132133
}
133134

0 commit comments

Comments
 (0)