chore(server): remove the MCP config re-export and the dormant row-shape helper

Only their own tests imported them. The MCP knobs are parsed in
app-config/parsers.ts, which tests them directly, and toCamelCase was never
wired into a plugin RPC handler.
This commit is contained in:
Maurice
2026-10-10 11:48:59 +02:00
parent 2050e796eb
commit db8babffc7
5 changed files with 1 additions and 247 deletions
+1 -3
View File
@@ -1,7 +1,6 @@
/**
* Pure env-string coercion helpers shared by the whole config layer (derive.ts,
* env.schema.ts) and — until their call sites migrate — by legacy readers like
* src/mcp/config.ts. Kept free of imports so units can test them in isolation.
* env.schema.ts). Kept free of imports so units can test them in isolation.
*
* Each helper reproduces a coercion family that already exists in the codebase.
* Parity is law: do NOT "fix" a family's quirks here (e.g. `numberOr` treating
@@ -102,7 +101,6 @@ export function parseDurationMs(value: string): number | null {
/**
* Session idle TTL in SECONDS via MCP_SESSION_TTL, default 1 hour, clamped to
* 24h so a milliseconds-value typo can't produce a 1000-hour session.
* (Same contract as src/mcp/config.ts, which commit 7 retires in favor of this.)
*/
export function resolveSessionTtlMs(raw: string | undefined): number {
const parsed = Number.parseInt(raw ?? '');
-7
View File
@@ -1,7 +0,0 @@
/**
* MCP tuning knobs from the environment (#1414). The parsers moved into the
* app-config layer (src/app-config/parsers.ts) — this module re-exports them
* so existing import paths keep working. mcp/index.ts consumes the derived
* values via readEnv().mcp.
*/
export { resolveSessionTtlMs, resolveKeepaliveMs } from '../app-config/parsers';
@@ -1,79 +0,0 @@
/**
* `toCamelCase` — the R-facade utility from Plan 3j's Task 6 (design spec D8, program
* `docs/superpowers/plans/2026-09-25-orm-phase3j-plugins.md`). It is DORMANT: built,
* tested and exported, but wired into ZERO of the 23 `*.rpc.ts` plugin-host handlers.
* `plugin-sdk/src/index.ts`'s published row types (`User`, `Place`, `Day`, `Reservation`,
* `PackingItem`, `TripFile`, `BudgetItem`, …) are snake_case today, and neither the SDK
* nor its tests carry any camelCase precedent — so applying this additively to a
* handler's return value would grow the plugin contract's visible surface with fields no
* published type, plugin author or test fixture expects, for no proven benefit. Whether
* TREK should start publishing camelCase copies going forward is a real product decision
* (see the plan's "For the user" note), not one an ORM-migration task makes unilaterally.
* This function exists so that decision, if it is ever made, is a one-line change at
* `HostSurfaceRpc#getUser`'s single return statement rather than new plumbing to design
* and land. It is a pure function — no class, no DI, no constructor — matching the
* `num`/`str`/`asPayload` shape in the sibling `rpc-params.ts` every one of the 23
* `*.rpc.ts` files already imports the same way, specifically so it never ripples through
* `tests/helpers/plugin-host.ts`/`rpc-host-deps.ts`'s hand-construction of all 23 classes.
*
* Scope, operate ONLY on a DB-row-shaped object the HOST itself assembled (e.g.
* `SELECT id, username, display_name, avatar FROM users …`). NEVER apply it to an opaque
* JSON-text value a PLUGIN wrote and the host merely parsed (`MetaRpc#get`/`#list`'s
* values, a plugin's own `config`) — a generic key transform would silently rewrite keys
* inside that stored blob, which is exactly why a generic deep-key transform was rejected
* project-wide (design spec D1/D8).
*/
/**
* Returns a shallow, additive camelCase COPY of `value`'s keys: every key containing `_`
* gets a camelCase sibling holding the same value (`display_name` -> `displayName`,
* `content_base64` -> `contentBase64`); the original snake_case key is never removed or
* replaced. A key with no underscore is left exactly as it is — no identity-transform
* copy is ever added for it. The conversion is ONE level deep only: a nested object, a
* `Date`, a `Buffer` or any other VALUE under a key is passed through completely
* unchanged (never recursed into, never spread) — only the row's own TOP-LEVEL key names
* are touched. This is the scope choice inventory §13 flagged as an open ambiguity in the
* design spec's "shallow" wording; a future caller wiring this in should expect a
* `user_profile: { first_name: 'A' }` row to gain `userProfile` pointing at the SAME
* nested object, with `first_name` inside it untouched — never a recursively-mapped
* `userProfile: { firstName: 'A' }`.
*
* An array is mapped element-wise, each element run back through `toCamelCase`. `null`,
* `undefined`, a `Date`, a `Buffer` and any other non-plain-object value passed as the
* top-level argument pass through completely unchanged, defensively — a caller mistake
* here (this is never called with anything but a row object in the codebase today, since
* it is unwired) must not throw into an RPC dispatch.
*/
export function toCamelCase<T extends Record<string, unknown>>(value: readonly T[]): (T & Record<string, unknown>)[];
export function toCamelCase<T extends Record<string, unknown>>(value: T): T & Record<string, unknown>;
export function toCamelCase<V>(value: V): V;
export function toCamelCase(value: unknown): unknown {
if (Array.isArray(value)) return value.map((item) => toCamelCase(item));
if (
value === null ||
typeof value !== 'object' ||
value instanceof Date ||
Buffer.isBuffer(value)
) {
return value;
}
const row = value as Record<string, unknown>;
const camelCopies: Record<string, unknown> = {};
for (const key of Object.keys(row)) {
if (!key.includes('_')) continue;
const camelKey = snakeKeyToCamel(key);
if (camelKey !== key) camelCopies[camelKey] = row[key];
}
return { ...row, ...camelCopies };
}
/**
* `display_name` -> `displayName`; `content_base64` -> `contentBase64`. A digit right
* after an underscore follows the same rule and needs no special case — uppercasing a
* digit is a no-op, so `place_2_id` -> `place2Id`.
*/
function snakeKeyToCamel(key: string): string {
return key.replace(/_([a-zA-Z0-9])/g, (_match, char: string) => char.toUpperCase());
}
-37
View File
@@ -1,37 +0,0 @@
/**
* MCP env tuning knobs (#1414): MCP_SESSION_TTL + MCP_SSE_KEEPALIVE parsing.
*/
import { describe, it, expect } from 'vitest';
import { resolveSessionTtlMs, resolveKeepaliveMs } from '../../../src/mcp/config';
describe('resolveSessionTtlMs', () => {
it('defaults to 1 hour when unset or invalid', () => {
expect(resolveSessionTtlMs(undefined)).toBe(60 * 60 * 1000);
expect(resolveSessionTtlMs('')).toBe(60 * 60 * 1000);
expect(resolveSessionTtlMs('nope')).toBe(60 * 60 * 1000);
expect(resolveSessionTtlMs('0')).toBe(60 * 60 * 1000);
expect(resolveSessionTtlMs('-5')).toBe(60 * 60 * 1000);
});
it('reads seconds from MCP_SESSION_TTL', () => {
expect(resolveSessionTtlMs('3600')).toBe(3600 * 1000);
expect(resolveSessionTtlMs('120')).toBe(120 * 1000);
});
it('clamps to 24h so a milliseconds typo cannot yield a 1000-hour session', () => {
expect(resolveSessionTtlMs('3600000')).toBe(24 * 60 * 60 * 1000);
});
});
describe('resolveKeepaliveMs', () => {
it('defaults to 25s when unset or invalid', () => {
expect(resolveKeepaliveMs(undefined)).toBe(25_000);
expect(resolveKeepaliveMs('abc')).toBe(25_000);
expect(resolveKeepaliveMs('-1')).toBe(25_000);
});
it('reads seconds and allows 0 to disable', () => {
expect(resolveKeepaliveMs('30')).toBe(30_000);
expect(resolveKeepaliveMs('0')).toBe(0);
});
});
@@ -1,121 +0,0 @@
/**
* `toCamelCase` (Plan 3j Task 6, design spec D8) — the DORMANT plugin-RPC facade
* utility. This is its entire test surface: it is wired into no `*.rpc.ts` handler, so
* no RPC-handler test changes alongside it. See `row-shape.ts`'s own docstring for why
* it exists unwired and what it must never be applied to.
*/
import { describe, it, expect, expectTypeOf } from 'vitest';
import { toCamelCase } from '../../../../src/nest/plugins/host/rpc-kit/row-shape';
describe('toCamelCase', () => {
it('ROWSHAPE-001 a flat row with several snake_case keys gets both cases, originals unchanged', () => {
const row = { id: 1, display_name: 'Ada', avatar_url: null as string | null };
const mapped = toCamelCase(row);
expect(mapped).toEqual({
id: 1,
display_name: 'Ada',
avatar_url: null,
displayName: 'Ada',
avatarUrl: null,
});
// Originals present and unchanged, not moved.
expect(mapped.display_name).toBe('Ada');
expect(mapped.avatar_url).toBeNull();
});
it('ROWSHAPE-002 a key with no underscore passes through untouched — no identity-transform copy', () => {
const row = { id: 1, username: 'ada' };
const mapped = toCamelCase(row);
expect(mapped).toEqual({ id: 1, username: 'ada' });
expect(Object.keys(mapped).sort()).toEqual(['id', 'username']);
});
it('ROWSHAPE-003 an array of rows maps element-wise', () => {
const rows = [
{ id: 1, display_name: 'Ada' },
{ id: 2, display_name: 'Bea' },
];
const mapped = toCamelCase(rows);
expect(mapped).toEqual([
{ id: 1, display_name: 'Ada', displayName: 'Ada' },
{ id: 2, display_name: 'Bea', displayName: 'Bea' },
]);
});
it('ROWSHAPE-004 a nested object stays untouched — proves the shallow, one-level scope', () => {
const profile = { first_name: 'Ada', last_name: 'Lovelace' };
const row = { id: 1, user_profile: profile };
const mapped = toCamelCase(row);
// The camel sibling points at the exact SAME object — never a recursively-mapped copy.
expect(mapped.userProfile).toBe(profile);
expect(mapped.user_profile).toBe(profile);
// The nested object itself gained no camelCase siblings.
expect(Object.keys(profile).sort()).toEqual(['first_name', 'last_name']);
expect((profile as Record<string, unknown>).firstName).toBeUndefined();
});
it('ROWSHAPE-005 null, undefined and a primitive pass through safely, unchanged', () => {
expect(toCamelCase(null)).toBeNull();
expect(toCamelCase(undefined)).toBeUndefined();
expect(toCamelCase('a string')).toBe('a string');
expect(toCamelCase(42)).toBe(42);
expect(toCamelCase(true)).toBe(true);
});
it('ROWSHAPE-006 a Date or Buffer value — at the top level or nested under a key — passes through unchanged', () => {
const when = new Date('2026-09-24T00:00:00.000Z');
const bytes = Buffer.from('hello');
// Top-level argument.
expect(toCamelCase(when)).toBe(when);
expect(toCamelCase(bytes)).toBe(bytes);
// Nested under a row key — never spread, never recursed into.
const row = { id: 1, created_at: when, blob_data: bytes };
const mapped = toCamelCase(row);
expect(mapped.createdAt).toBe(when);
expect(mapped.created_at).toBe(when);
expect(mapped.blobData).toBe(bytes);
expect(mapped.blob_data).toBe(bytes);
});
it('ROWSHAPE-007 a digit immediately after an underscore follows the same rule (no special case)', () => {
const row = { place_2_id: 7 };
const mapped = toCamelCase(row);
expect(mapped).toEqual({ place_2_id: 7, place2Id: 7 });
});
it('ROWSHAPE-008 an empty object round-trips to an equal empty object', () => {
expect(toCamelCase({})).toEqual({});
});
it('ROWSHAPE-009 an empty array round-trips to an empty array', () => {
expect(toCamelCase([])).toEqual([]);
});
it('ROWSHAPE-010 type-level: the SDK-shaped User row keeps its snake_case members AND exposes the camel siblings', () => {
// Mirrors plugin-sdk/src/index.ts's published `User` shape exactly (id, username,
// display_name, avatar, plus its index signature) — the one row HostSurfaceRpc#getUser
// (HR1) would wrap if the facade were ever wired in.
interface SdkUserRow {
id: number;
username?: string;
display_name?: string | null;
avatar?: string | null;
[k: string]: unknown;
}
const row: SdkUserRow = { id: 1, username: 'ada', display_name: 'Ada Lovelace', avatar: null };
const mapped = toCamelCase(row);
// The mapped type is `SdkUserRow & Record<string, unknown>`: every original member
// stays typed, and any camelCase sibling (`displayName`, unlisted on `SdkUserRow`
// itself) resolves through the added index signature rather than failing to compile —
// exactly the shape a plugin-sdk follow-up release's row types would need.
expectTypeOf(mapped).toEqualTypeOf<SdkUserRow & Record<string, unknown>>();
expectTypeOf(mapped.display_name).toEqualTypeOf<SdkUserRow['display_name']>();
expectTypeOf(mapped.displayName).toEqualTypeOf<unknown>();
expect(mapped.display_name).toBe('Ada Lovelace');
expect(mapped.displayName).toBe('Ada Lovelace');
});
});