Harden generator against Codex review findings

- A SessionEventMap member that is not a property signature with an
  explicit payload type is now a hard error instead of silently skipped —
  a method-form or type-less member joins keyof SessionEventMap and must
  not escape the catalog.
- A top-level interface SessionEventMap outside @deepseek-ai/dsh-session
  (ownership read from the package manifest) is now a hard error — an
  unrelated same-named local interface was previously catalogued as the
  on-disk vocabulary.
- JSDoc tag detection runs on the trimmed line, so an extra-indented
  '*  @mode' can no longer bypass the forbidden-tag check and leak into
  prose.

Four new spec cases cover these; RFC and module doc updated to describe
the enforced (not just assumed) invariants.
This commit is contained in:
Tianyi Cui
2026-07-04 23:11:42 +08:00
parent 232f314c3a
commit 53dc3c8a28
3 changed files with 91 additions and 13 deletions

View File

@@ -50,9 +50,13 @@ afterEach(() => {
while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
})
/** The manifest that marks a fixture package as the owning session package. */
const OWNER_MANIFEST = '{ "name": "@deepseek-ai/dsh-session" }\n'
describe('gen-persistence-catalog collectLogEvents', () => {
it('extracts a documented member of the owning top-level interface', () => {
const events = collectLogEvents(make({
'packages/core/fix/package.json': OWNER_MANIFEST,
'packages/core/fix/src/types.ts':
'export interface SessionEventMap {\n /** A thing was recorded. */\n \'fix/happened\': { turn: number }\n}\n',
}))
@@ -66,6 +70,14 @@ describe('gen-persistence-catalog collectLogEvents', () => {
})
})
it('hard-errors on a top-level interface outside the owning package', () => {
expect(() => collectLogEvents(make({
'packages/group/alien/package.json': '{ "name": "@deepseek-ai/dsh-alien" }\n',
'packages/group/alien/src/types.ts':
'export interface SessionEventMap {\n /** Not the real vocabulary. */\n \'alien/event\': { turn: number }\n}\n',
}))).toThrow(/top-level interface SessionEventMap .* is outside @deepseek-ai\/dsh-session \(package @deepseek-ai\/dsh-alien\)/)
})
it('extracts a member declaration-merged via the session module', () => {
const events = collectLogEvents(make({
'packages/group/fix/src/types.ts': merge(' /** Merged provenance. */\n \'fix/merged\': { id: string }'),
@@ -95,6 +107,24 @@ describe('gen-persistence-catalog collectLogEvents', () => {
}))).toThrow(/carries an @mode tag/)
})
it('hard-errors on an extra-indented @mode tag (does not leak into prose)', () => {
expect(() => collectLogEvents(make({
'packages/group/fix/src/types.ts': merge(' /**\n * Documented, but mistagged.\n * @mode emit\n */\n \'fix/indented\': { turn: number }'),
}))).toThrow(/carries an @mode tag/)
})
it('hard-errors on a method-form member (it still joins keyof SessionEventMap)', () => {
expect(() => collectLogEvents(make({
'packages/group/fix/src/types.ts': merge(' /** Documented, wrong shape. */\n \'fix/method\'(turn: number): void'),
}))).toThrow(/not a property signature with an explicit payload type/)
})
it('hard-errors on a property member with no payload type annotation', () => {
expect(() => collectLogEvents(make({
'packages/group/fix/src/types.ts': merge(' /** Documented, no payload. */\n \'fix/bare\''),
}))).toThrow(/not a property signature with an explicit payload type/)
})
it('hard-errors on a non-literal member name', () => {
expect(() => collectLogEvents(make({
'packages/group/fix/src/types.ts': merge(' /** Not a literal. */\n unquoted: { turn: number }'),