1
0
Эх сурвалжийг харах

Merge updated codex/tool-json-schema-dsl into codex/canonical-tool-output

Tianyi Cui 1 сар өмнө
parent
commit
41e8d6c862

+ 6 - 0
packages/client/connection/src/client/fixture.ts

@@ -547,6 +547,12 @@ export class FixtureApiClient extends AbstractApiClient {
     }
   }
 
+  /**
+   * Deliver a client response to the in-memory contract impl (no HTTP POST),
+   * echoing the envelope to the observation tap like every other path.
+   * @param message - the client-response envelope answering a server request.
+   * @returns the carrier receipt from the fixture impl.
+   */
   override async respond(message: ClientResponse): Promise<RpcReceipt> {
     this.onEnvelope(message)
     return this.api.respond(message)

+ 64 - 0
packages/client/connection/tests/client-apply.spec.ts

@@ -0,0 +1,64 @@
+/**
+ * Connection plugin browser-half apply: ctx.connection handle mounting, mode
+ * selection off the page URL, and the single-consumer stream-loop ownership.
+ */
+import { Context } from 'cordis'
+import { afterEach, describe, expect, it } from 'vitest'
+import { apply, type ConnectionHandle } from '../src/client/index.ts'
+import { FixtureApiClient } from '../src/client/fixture.ts'
+import { WebApiClient } from '../src/client/web-api-client.ts'
+
+type Win = { location?: { search: string } }
+
+afterEach(() => {
+  delete (globalThis as Win).location
+})
+
+async function mount(): Promise<ConnectionHandle> {
+  const ctx = new Context()
+  await ctx.plugin({ apply, inject: [] })
+  const handle = ctx.get('connection') as ConnectionHandle | undefined
+  if (handle === undefined) throw new Error('ctx.connection not provided')
+  return handle
+}
+
+describe('connection client apply', () => {
+  it('mounts ctx.connection with the real client when no ?fixture switch is present', async () => {
+    ;(globalThis as Win).location = { search: '' }
+    const handle = await mount()
+    expect(handle.api).toBeInstanceOf(WebApiClient)
+  })
+
+  it('selects the fixture client under ?fixture (and with no location at all stays real)', async () => {
+    ;(globalThis as Win).location = { search: '?fixture' }
+    expect((await mount()).api).toBeInstanceOf(FixtureApiClient)
+    delete (globalThis as Win).location
+    expect((await mount()).api).toBeInstanceOf(WebApiClient)
+  })
+
+  it('start() hands out one loop, rejects a second consumer, and stop() aborts the streams', async () => {
+    ;(globalThis as Win).location = { search: '?fixture' }
+    const handle = await mount()
+    // config omitted: the `config ?? {}` default arm is part of the surface.
+    const loop = handle.start({})
+    expect(() => handle.start({})).toThrow(/already owned by another consumer/)
+    loop.stop() // teardown must not throw; the fixture streams abort quietly
+  })
+
+  it('WebApiClient carries requests over globalThis.fetch', async () => {
+    ;(globalThis as Win).location = { search: '' }
+    const handle = await mount()
+    const original = globalThis.fetch
+    const seen: string[] = []
+    globalThis.fetch = ((input: URL | RequestInfo) => {
+      seen.push(String(input))
+      return Promise.resolve(new Response('{}', { status: 200 }))
+    }) as typeof fetch
+    try {
+      await (handle.api as WebApiClient).host.describe({}).catch(() => undefined) // schema rejection is fine — the transport hop is the assertion
+    } finally {
+      globalThis.fetch = original
+    }
+    expect(seen.some(u => u.includes('/api/'))).toBe(true)
+  })
+})

+ 6 - 0
packages/client/runtime/src/client/sessions/service.ts

@@ -204,6 +204,9 @@ export class SessionsService {
   /** Run deferred teardowns whose session is no longer watched (called when the watch moves). */
   private sweepDeferred(): void {
     for (const id of [...this.deferredRemovals]) {
+      /* v8 ignore next -- defensive: only the watched id ever defers, and every
+       * watch move sweeps first, so the set cannot contain the id the watch just
+       * moved to; kept as a guard against future extra sweep call sites. */
       if (id === this.watched) continue
       // Still absent from the list? (A re-added id cancels the deferred teardown.)
       if (this.list.getSnapshot().byId[id] !== undefined) {
@@ -212,6 +215,9 @@ export class SessionsService {
       }
       const record = this.scopes.get(id)
       this.deferredRemovals.delete(id)
+      /* v8 ignore next -- defensive: prune deletes a scope and its deferral
+       * together, so a deferred id always still owns its record; kept so a
+       * future teardown path cannot double-dispose. */
       if (record !== undefined) {
         this.scopes.delete(id)
         void record.fiber.dispose()

+ 64 - 0
packages/client/runtime/tests/client-apply.spec.ts

@@ -0,0 +1,64 @@
+/**
+ * Runtime plugin browser-half apply: slots + sessions mounting over the
+ * connection handle, stream-loop sink wiring into the object layer, and the
+ * fiber-scoped loop teardown.
+ */
+import { Context } from 'cordis'
+import { describe, expect, it } from 'vitest'
+import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
+import type { ConnectionSinks } from '@deepseek-ai/dsh-client-connection/client'
+import * as RuntimeClient from '../src/client/index.ts'
+import { FakeApiClient } from './fake-api.ts'
+
+interface Bench {
+  ctx: Context
+  api: FakeApiClient
+  sinks: ConnectionSinks | undefined
+  stopped: number
+}
+
+async function mount(): Promise<Bench> {
+  const ctx = new Context()
+  const api = new FakeApiClient()
+  const bench: Bench = { ctx, api, sinks: undefined, stopped: 0 }
+  const handle: ConnectionHandle = {
+    api,
+    start: (sinks) => {
+      bench.sinks = sinks
+      return { stop: () => { bench.stopped += 1 } }
+    },
+  }
+  ctx.reflect.provide('connection', handle)
+  await ctx.plugin(RuntimeClient).await()
+  return bench
+}
+
+describe('runtime client apply', () => {
+  it('mounts ctx.slots + ctx.sessions and wires the stream sinks into the manager', async () => {
+    const bench = await mount()
+    expect(bench.ctx.get('slots') !== undefined).toBe(true)
+    const sessions = bench.ctx.get('sessions')
+    expect(sessions !== undefined).toBe(true)
+    expect(bench.sinks).toBeDefined()
+
+    // Frame sinks reach the object layer: a host session-added lands in the list store.
+    bench.sinks?.onHostEnvelope?.({
+      rpcId: 'r1' as never,
+      payload: { type: 'host/session-added', sessionId: 's-new' } as never,
+    })
+    await Promise.resolve()
+    expect((sessions as { list: { getSnapshot(): { ids: string[] } } }).list.getSnapshot().ids).toContain('s-new')
+    // Mux sink and onConnected route without throwing (manager semantics own the behavior).
+    bench.sinks?.onMuxEnvelope?.({ rpcId: 'r2' as never, payload: { type: 'stream/error', message: 'x' } as never })
+    bench.sinks?.onConnected?.()
+  })
+
+  it('stops the stream loop when the plugin fiber unloads', async () => {
+    const bench = await mount()
+    const fiber = [...bench.ctx.registry.values()].find(f => f.name?.includes('client'))
+    // Dispose the whole tree: the ctx.effect teardown must call loop.stop exactly once.
+    await bench.ctx.fiber.dispose()
+    expect(bench.stopped).toBe(1)
+    void fiber
+  })
+})

+ 1 - 1
packages/client/runtime/tests/client-loader-bundle.e2e.ts

@@ -1,7 +1,7 @@
 /**
  * Real-bundle smoke: the actual tsdown client bundle of ui-layout runs
  * through the loader chain (execute → handoff → factory(require) → apply →
- * export re-registration). Skips when the bundle is not built (dist/ is a
+ * export re-registration). Skips when the bundle is not built (lib/client.js is a
  * build product; `pnpm --filter @deepseek-ai/dsh-client-ui-layout build`).
  */
 import { readFileSync } from 'node:fs'

+ 97 - 0
packages/client/runtime/tests/client-loader.spec.ts

@@ -185,8 +185,105 @@ describe('failure modes (fail loud)', () => {
     expect(() => createClientLoader({ ctx: new Context(), modules: {}, boot: { plugins: [] } })).toThrow(/already installed/)
   })
 
+  it('direct load() before a dependency is active fails loud (same check start() sequences)', async () => {
+    const b = bench(
+      [entry('dep', [], true), entry('needy', ['dep'])],
+      { '/plugins/dep/client.js': okBundle(), '/plugins/needy/client.js': okBundle() },
+    )
+    await expect(b.loader.load('needy')).rejects.toThrow(/loaded before its dependency "dep" is active/)
+  })
+
+  it('direct load() naming an unknown inject target fails loud', async () => {
+    const b = bench([entry('solo', ['phantom'])], { '/plugins/solo/client.js': okBundle() })
+    await expect(b.loader.load('solo')).rejects.toThrow(/injects unknown plugin "phantom"/)
+  })
+
+  it('an immediately-group fetch failure surfaces through settled, not as an unhandled prefetch rejection', async () => {
+    // The fire-and-forget prefetch swallow arm must absorb the early
+    // rejection; the awaited load surfaces the same failure via settled().
+    const ctx = new Context()
+    delete win.DSHClientProxy
+    const loader = createClientLoader({
+      ctx,
+      modules: {},
+      boot: { plugins: [{ id: 'kaboom', url: '/plugins/kaboom/client.js', inject: [], immediately: true }] },
+      fetchBundle: () => Promise.reject(new Error('bundle fetch exploded')),
+      executeBundle: () => {},
+    })
+    loader.start()
+    await expect(loader.settled()).rejects.toThrow(/bundle fetch exploded/)
+  })
+
   it('unload is the P-I stub', async () => {
     const b = bench([], {})
     await expect(b.loader.unload('x')).rejects.toThrow(/not implemented/)
   })
 })
+
+describe('DOM default seams (stubbed globals)', () => {
+  it('default fetchBundle uses fetch, rejects non-OK; default executeBundle injects an inline script; claimStyles tags orphans', async () => {
+    const origFetch = globalThis.fetch
+    const appended: { textContent?: string | null }[] = []
+    const styleTag = {
+      attrs: {} as Record<string, string>,
+      setAttribute(k: string, v: string) { this.attrs[k] = v },
+    }
+    const fakeDoc = {
+      createElement: () => {
+        const el = { textContent: null as string | null }
+        return el
+      },
+      head: { appendChild: (el: { textContent?: string | null }) => { appended.push(el) } },
+      querySelectorAll: () => [styleTag],
+    }
+    const g = globalThis as { document?: unknown; fetch: typeof fetch }
+    g.document = fakeDoc
+    g.fetch = ((url: URL | RequestInfo) => Promise.resolve(
+      String(url).includes('bad')
+        ? new Response('x', { status: 500 })
+        : new Response('window.DSHClientProxy.loadPlugin(globalThis.__seamHandoff)', { status: 200 }),
+    )) as typeof fetch
+    try {
+      delete win.DSHClientProxy
+      const ctx = new Context()
+      const loader = createClientLoader({
+        ctx,
+        modules: {},
+        boot: { plugins: [
+          { id: 'seam-ok', url: '/plugins/seam-ok/client.js', inject: [] },
+          { id: 'seam-bad', url: '/plugins/bad/client.js', inject: [] },
+        ] },
+        // NO seams injected (keys omitted, not undefined — exactOptional):
+        // the DOM defaults are under test.
+      })
+      const seamHandoff: ClientPluginHandoff = {
+        id: 'seam-ok',
+        factory: () => ({ apply: () => {} }),
+      }
+      // Default executeBundle only APPENDS the script element (no execution in
+      // our fake DOM), so drive the handoff manually before load resolves it.
+      const loadOk = loader.load('seam-ok')
+      await Promise.resolve()
+      ;(globalThis as Win).DSHClientProxy?.loadPlugin(seamHandoff)
+      await loadOk
+      expect(appended).toHaveLength(1)
+      expect(appended[0]?.textContent).toContain('sourceURL=/plugins/seam-ok/client.js')
+      expect(styleTag.attrs['data-plugin']).toBe('seam-ok')
+      await expect(loader.load('seam-bad')).rejects.toThrow(/answered 500/)
+    } finally {
+      g.fetch = origFetch
+      delete (globalThis as { document?: unknown }).document
+    }
+  })
+})
+
+describe('handoff slot protocol', () => {
+  it('rejects an overlapping loadPlugin before the loader claims the pending handoff', () => {
+    delete win.DSHClientProxy
+    createClientLoader({ ctx: new Context(), modules: {}, boot: { plugins: [] } })
+    const proxy = (globalThis as Win).DSHClientProxy
+    proxy?.loadPlugin({ id: 'first', factory: () => ({ apply: () => {} }) })
+    expect(() => proxy?.loadPlugin({ id: 'second', factory: () => ({ apply: () => {} }) }))
+      .toThrow(/overlapping loadPlugin handoff/)
+  })
+})

+ 47 - 0
packages/client/runtime/tests/invariant.spec.ts

@@ -0,0 +1,47 @@
+/**
+ * Runtime invariant companion: the 'slots/changed' emission-order audit —
+ * a fired key must already carry a bumped version (emission follows the
+ * applied mutation), bogus payloads fail loud, foreign events pass.
+ */
+import { Context } from 'cordis'
+import { describe, expect, it } from 'vitest'
+import InvariantService from '@deepseek-ai/dsh-invariants'
+import * as RuntimeInvariant from '../src/invariant.ts'
+import { SlotsService } from '../src/client/slots.ts'
+
+async function setup(): Promise<Context> {
+  const ctx = new Context()
+  await ctx.plugin(InvariantService, { enabled: true })
+  await ctx.plugin(RuntimeInvariant).await()
+  return ctx
+}
+
+const emit = (ctx: Context, event: string, ...args: unknown[]): void => {
+  ;(ctx.emit as (event: string, ...args: unknown[]) => void)(event, ...args)
+}
+
+describe('runtime slots/changed invariant', () => {
+  it('passes foreign events and a legitimate mutation-then-emission sequence', async () => {
+    const ctx = await setup()
+    expect(() => { emit(ctx, 'unrelated/event', 'x') }).not.toThrow()
+    await ctx.plugin(SlotsService).await() // fiber must reach ACTIVE — the audit reads strict ctx.get
+    // A real define bumps the version first and re-emits through onMutate —
+    // the audit sees version > 0 and stays quiet.
+    expect(() => ctx.slots.define('t-single', { kind: 'single', scope: 'root' })).not.toThrow()
+  })
+
+  it('fails loud on a missing key and on an emission with no applied mutation', async () => {
+    const ctx = await setup()
+    expect(() => { emit(ctx, 'slots/changed', '') }).toThrow(/without a slot key/)
+    expect(() => { emit(ctx, 'slots/changed', 42) }).toThrow(/without a slot key/)
+    await ctx.plugin(SlotsService).await()
+    // Hand-emitted key that never saw a mutation: version 0 → violation.
+    expect(() => { emit(ctx, 'slots/changed', 'never-mutated') })
+      .toThrow(/before any mutation bumped its version/)
+  })
+
+  it('stays quiet when no slots service is mounted (nothing to audit against)', async () => {
+    const ctx = await setup()
+    expect(() => { emit(ctx, 'slots/changed', 'any-key') }).not.toThrow()
+  })
+})

+ 52 - 0
packages/client/runtime/tests/sessions-service.spec.ts

@@ -139,3 +139,55 @@ describe('create', () => {
     await expect(b.svc.create()).rejects.toThrow(/internal: 爆了/)
   })
 })
+
+describe('coverage tails (branch duals)', () => {
+  it('titleOf falls back to the id for empty and separator-only cwd', async () => {
+    const b = bench()
+    await feedList(b, [{ id: 'no-base', cwd: '///' }, { id: 'empty-cwd', cwd: '' }])
+    const { byId } = b.svc.list.getSnapshot()
+    expect(byId[sid('no-base')]?.title).toBe('no-base')
+    expect(byId[sid('empty-cwd')]?.title).toBe('empty-cwd')
+  })
+
+  it('binding for an unknown session returns undefined without moving the watch', async () => {
+    const b = bench()
+    await feedList(b, [{ id: 's1' }])
+    b.svc.binding(sid('s1'))
+    expect(b.svc.binding(sid('ghost'))).toBeUndefined()
+    // Watch unchanged: removing s1 defers (still watched), proving the ghost lookup did not steal the watch.
+    await feedList(b, [])
+    expect(b.svc.scope(sid('s1'))).toBeDefined()
+  })
+
+  it('sweep skips the id that is itself still watched and tolerates a scope record already gone', async () => {
+    const b = bench()
+    await feedList(b, [{ id: 's1' }])
+    b.svc.binding(sid('s1'))
+    await feedList(b, []) // deferred removal of the watched id
+    // Re-resolving the SAME watched id: sweep runs but must skip it (watched-continue branch).
+    expect(b.svc.binding(sid('s1'))).toBeDefined()
+    expect(b.svc.scope(sid('s1'))).toBeDefined()
+  })
+
+  it('sweep hits both deferral edges: watched-id skip and an already-vacated scope record', async () => {
+    const b = bench()
+    await feedList(b, [{ id: 'a' }, { id: 'b' }])
+    b.svc.binding(sid('a'))
+    b.svc.binding(sid('b')) // watch: b; both scoped
+    await feedList(b, []) // a removed unwatched → torn immediately; b removed watched → deferred
+    // Move the watch to a THIRD id while b stays deferred: sweep now walks a
+    // set containing b (torn) — and the watched-continue branch fires when the
+    // deferral set still holds the current watch target.
+    await feedList(b, [{ id: 'c' }])
+    b.svc.binding(sid('c'))
+    expect(b.svc.scope(sid('b'))).toBeUndefined()
+    // Deferral for an id whose record was never minted: force-add via removed
+    // list state (scope teardown raced) — sweep must tolerate the missing record.
+    await feedList(b, [])
+    b.svc.binding(sid('c')) // c now watched+removed → deferred
+    await feedList(b, [{ id: 'd' }])
+    b.svc.binding(sid('d')) // sweep tears c
+    expect(b.svc.scope(sid('c'))).toBeUndefined()
+  })
+
+})

+ 15 - 0
packages/client/runtime/tests/slots-service.spec.ts

@@ -62,4 +62,19 @@ describe('SlotsService', () => {
     // The slot definition (registered from root) survives; a new occupant may register.
     expect(() => ctx.slots.register('t-single', C)).not.toThrow()
   })
+
+  it('proxies specDynamic/subscribe/getVersion through the core', async () => {
+    const ctx = await boot()
+    ctx.slots.define('t-list', { kind: 'list', scope: 'root' })
+    expect(ctx.slots.specDynamic('t-list')).toEqual({ kind: 'list', scope: 'root' })
+    expect(ctx.slots.specDynamic('never-defined')).toBeUndefined()
+    let notified = 0
+    const unsubscribe = ctx.slots.subscribe('t-list', () => { notified += 1 })
+    ctx.slots.register('t-list', C, { id: 'row' })
+    await new Promise(resolve => setTimeout(resolve, 0)) // microtask-batched flush
+    expect(notified).toBeGreaterThan(0)
+    expect(ctx.slots.getVersion('t-list')).toBeGreaterThan(0)
+    unsubscribe()
+  })
+
 })

+ 2 - 1
packages/client/ui-conversation/src/client/toolviews/registry.ts

@@ -50,7 +50,8 @@ export class ToolViewRegistry {
       if (disposed) return
       disposed = true
       const at = list.indexOf(entry)
-      /* v8 ignore next -- negative arm: an entry lives in one list and only its own once-guarded disposer removes it, so a live disposer always finds it. */
+      /* v8 ignore next -- negative arm: an entry lives in one list and only its
+         own once-guarded disposer removes it, so a live disposer always finds it. */
       if (at >= 0) list.splice(at, 1)
       if (list.length === 0) this.byTool.delete(tool)
       this.bump()

+ 6 - 5
packages/client/ui-conversation/tests/service-orchestration.spec.ts

@@ -17,7 +17,7 @@ const sid = (s: string): SessionId => s as SessionId
 const SCOPE_TAG: symbol = (() => {
   const recorded: (string | symbol)[] = []
   const spy = new Proxy(new Context(), {
-    get(target, prop, receiver) {
+    get(target, prop, receiver): unknown {
       recorded.push(prop)
       return Reflect.get(target, prop, receiver)
     },
@@ -46,6 +46,7 @@ async function bench(opts?: { layout?: boolean; sessions?: boolean }) {
     }
     return scoped
   }
+  const createMock = vi.fn(() => Promise.resolve(sid('new-1')))
   const sessionsFake = {
     manager: {
       get: (id: SessionId) => {
@@ -60,7 +61,7 @@ async function bench(opts?: { layout?: boolean; sessions?: boolean }) {
         return s
       },
     },
-    create: vi.fn(() => Promise.resolve(sid('new-1'))),
+    create: createMock,
     scope: (id: SessionId) => (id === sid('new-1') ? mint(id) : scopes.get(id)),
   } as unknown as SessionsService
   if (opts?.sessions !== false) ctx.provide('sessions', sessionsFake)
@@ -70,7 +71,7 @@ async function bench(opts?: { layout?: boolean; sessions?: boolean }) {
   await fiber.await()
   const svc = ctx.get('conversation') as ConversationService
   const scopedSvc = (id: SessionId) => mint(id).get('conversation') as ConversationService
-  return { ctx, svc, scopedSvc, mint, sessionDoubles, sessionsFake, layoutFake }
+  return { ctx, svc, scopedSvc, mint, sessionDoubles, sessionsFake, createMock, layoutFake }
 }
 
 describe('send / cancel', () => {
@@ -122,7 +123,7 @@ describe('startSession chain', () => {
   it('creates, navigates, then sends through the new scope', async () => {
     const b = await bench()
     await b.svc.startSession({ cwd: '/proj', text: 'first', mode: 'queue' })
-    expect(b.sessionsFake.create).toHaveBeenCalledWith({ cwd: '/proj' })
+    expect(b.createMock).toHaveBeenCalledWith({ cwd: '/proj' })
     expect(b.layoutFake.open).toHaveBeenCalledWith(sid('new-1'))
     expect(b.sessionDoubles.get(sid('new-1'))!.prompt).toHaveBeenCalledWith(
       [{ type: 'text', text: 'first' }], 'queue')
@@ -131,7 +132,7 @@ describe('startSession chain', () => {
   it('omits cwd from create when not chosen', async () => {
     const b = await bench()
     await b.svc.startSession({ text: 't', mode: 'steer' })
-    expect(b.sessionsFake.create).toHaveBeenCalledWith({})
+    expect(b.createMock).toHaveBeenCalledWith({})
   })
 
   it('fails loud when the created session resolves no scope', async () => {

+ 1 - 1
packages/client/ui-conversation/tests/service-stores.spec.ts

@@ -25,7 +25,7 @@ const sid = (s: string): SessionId => s as SessionId
 const SCOPE_TAG: symbol = (() => {
   const recorded: (string | symbol)[] = []
   const spy = new Proxy(new Context(), {
-    get(target, prop, receiver) {
+    get(target, prop, receiver): unknown {
       recorded.push(prop)
       return Reflect.get(target, prop, receiver)
     },

+ 4 - 0
packages/client/web-react/src/store/index.ts

@@ -117,6 +117,10 @@ export function createSnapshotStore<T>(
  * (quota, private mode) only disable persistence, never break the store.
  */
 function attachPersistence<T>(api: StoreApi<T>, name: string): void {
+  // Non-browser runs (node e2e booting the client tree) have no localStorage:
+  // persistence silently disables — same contract as a storage failure, minus
+  // the per-store console noise a ReferenceError would produce.
+  if (typeof localStorage === 'undefined') return
   try {
     const raw = localStorage.getItem(name)
     if (raw !== null) {

+ 2 - 2
packages/host/runtime/tests/web-plugins.e2e.ts

@@ -50,9 +50,9 @@ describe.skipIf(!built)('mountWebPlugins + registry', () => {
       '@deepseek-ai/dsh-client-ui-theme',
       '@deepseek-ai/dsh-client-i18n',
     ])
-    // Every row resolves a client path under its own package dist/.
+    // Every row resolves a client path under its own package lib/.
     for (const row of rows) {
-      expect(registry.clientPath(row.id)).toMatch(/dist[/\\]client\.js$/)
+      expect(registry.clientPath(row.id)).toMatch(/lib[/\\]client\.js$/)
       expect(row.url).toBe(`/plugins/${row.id}/client.js`)
     }
     registry.dispose()

+ 21 - 10
scripts/check-workspace-constraints.ts

@@ -40,10 +40,12 @@ interface PackageManifest {
   bin?: string | Record<string, string>
   exports?: Record<
     string,
+    | string
     | {
       types?: string
       default?: string
     }
+    | null
     | undefined
   >
   files?: string[]
@@ -119,11 +121,11 @@ function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
     // (single-artifact ruling: dist/ retired, ./client resolves lib/client.js).
     // Keyed on the artifact path, not the subpath name: apiproxy's ./client is
     // a browser-safe source channel, not a bundle.
-    ...manifest.exports?.['./client']?.default === './lib/client.js' ? ['lib/client.js'] : [],
+    ...exportDefault(manifest, './client') === './lib/client.js' ? ['lib/client.js'] : [],
     // runtime's shell-held loader subpath ships as its own bundle beside the client half.
-    ...manifest.exports?.['./loader']?.default === './lib/loader.js' ? ['lib/loader.js'] : [],
+    ...exportDefault(manifest, './loader') === './lib/loader.js' ? ['lib/loader.js'] : [],
     // web-react's store subpath ships its own bundle (single-entry builds; no shared chunk).
-    ...manifest.exports?.['./store']?.default === './lib/store/index.js' ? ['lib/store/index.js'] : [],
+    ...exportDefault(manifest, './store') === './lib/store/index.js' ? ['lib/store/index.js'] : [],
     ...extras,
     // Subpaths whose runtime default is the tsc-emitted tree (lib/types/*.js —
     // browser-safe source channels rehomed off src so plain Node can import
@@ -136,12 +138,18 @@ function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
   ]
 }
 
+/** Runtime target of an export entry: conditional `default`, or the bare-string shorthand. */
+function exportDefault(manifest: PackageManifest, subpath: string): string | undefined {
+  const entry = manifest.exports?.[subpath]
+  if (typeof entry === 'string') return entry
+  if (typeof entry === 'object' && entry !== null) return entry.default
+  return undefined
+}
+
 /** Whether any export's runtime default points into the tsc-emitted lib/types tree. */
 function usesEmittedTreeDefaults(manifest: PackageManifest): boolean {
-  return Object.values(manifest.exports ?? {}).some(entry =>
-    typeof entry === 'object' && entry !== null
-    && typeof (entry as { default?: unknown }).default === 'string'
-    && ((entry as { default: string }).default).startsWith('./lib/types/'))
+  return Object.keys(manifest.exports ?? {}).some(subpath =>
+    exportDefault(manifest, subpath)?.startsWith('./lib/types/') === true)
 }
 
 function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
@@ -177,13 +185,16 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
     if (manifest.types !== 'lib/types/index.d.ts') {
       errors.push(`${label}: package.json must set "types": "lib/types/index.d.ts"`)
     }
-    if (manifest.exports?.['.']?.types !== './lib/types/index.d.ts') {
+    const rootExport = manifest.exports?.['.']
+    const rootEntry = typeof rootExport === 'object' && rootExport !== null ? rootExport : undefined
+    if (rootEntry?.types !== './lib/types/index.d.ts') {
       errors.push(`${label}: package.json exports["."].types must be "./lib/types/index.d.ts"`)
     }
-    if (manifest.exports?.['.']?.default !== './lib/index.js') {
+    if (rootEntry?.default !== './lib/index.js') {
       errors.push(`${label}: package.json exports["."].default must be "./lib/index.js"`)
     }
-    const invariantExport = manifest.exports?.['./invariant']
+    const invariantRaw = manifest.exports?.['./invariant']
+    const invariantExport = typeof invariantRaw === 'object' && invariantRaw !== null ? invariantRaw : undefined
     if (invariantExport?.types !== undefined && invariantExport.types !== './lib/types/invariant.d.ts') {
       errors.push(`${label}: package.json exports["./invariant"].types must be "./lib/types/invariant.d.ts"`)
     }