Преглед изворни кода

fix(code-runtime-python): make the wire-field binding exhaustive over interface keys

The array-based FrameFields<T> only checked that listed names were members of
the frame's keys, so a field added to a TS interface (e.g. LogMessage.seq?)
left the existing arrays a valid subset — typecheck passed, and since the
constant and Python both lacked the field the mirror test passed too. The
JSDoc's claim that runtime covered this was false.

Replace it with WIRE_FRAME_FIELD_ROLES, a per-frame map keyed by field name
(`Record<RequiredKeys<T>, 'required'> & Record<OptionalKeys<T>, 'optional'>`),
so every interface key MUST appear with a matching required/optional tag: an
added field, a removed field, a rename, or an optionality flip all fail
typecheck at the roles map (verified). WIRE_FRAME_FIELDS is projected from it
as the sorted arrays the mirror test still compares to the Python TypedDicts.

Also drop the `export` added to the promoted frame interfaces (Namespace,
ErrorClass, RunMessage, DoneErrorField, ReplyOk, ReplyErr) — nothing outside
protocol.ts imports them, so the barrel surface is unchanged and knip stays
clean.
Chinesezjc пре 2 месеци
родитељ
комит
adba2e305a
1 измењених фајлова са 65 додато и 53 уклоњено
  1. 65 53
      packages/code-runtime/code-runtime-python/src/protocol.ts

+ 65 - 53
packages/code-runtime/code-runtime-python/src/protocol.ts

@@ -16,7 +16,7 @@
  * the program-visible name the namespace is materialized under; `errorClass`,
  * when present, asks the bootstrap to mint a program-visible exception class.
  */
-export interface Namespace {
+interface Namespace {
   global: string
   names: string[]
   errorClass?: ErrorClass
@@ -26,7 +26,7 @@ export interface Namespace {
  * A namespace's program-visible exception class: rejected calls raise its
  * instances carrying the failed member name on `memberNameProperty`.
  */
-export interface ErrorClass {
+interface ErrorClass {
   name: string
   memberNameProperty: string
 }
@@ -55,7 +55,7 @@ export interface BootMessage {
 }
 
 /** Host → Python: sent after `boot-ack`; carries only the model's program body. */
-export interface RunMessage {
+interface RunMessage {
   type: 'run'
   program: string
 }
@@ -99,7 +99,7 @@ interface LogMessage {
 }
 
 /** The failure carried on a {@link DoneMessage}: one of three kinds plus text. */
-export interface DoneErrorField {
+interface DoneErrorField {
   kind: 'exception' | 'invalid-output' | 'output-limit'
   message: string
 }
@@ -129,7 +129,7 @@ interface DoneMessage {
 export type ChildToHost = BootAckMessage | CallMessage | LogMessage | DoneMessage
 
 /** Host → Python: successful answer to one {@link CallMessage}. */
-export interface ReplyOk {
+interface ReplyOk {
   type: 'reply'
   id: number
   ok: true
@@ -137,7 +137,7 @@ export interface ReplyOk {
 }
 
 /** Host → Python: failed answer to one {@link CallMessage}. */
-export interface ReplyErr {
+interface ReplyErr {
   type: 'reply'
   id: number
   ok: false
@@ -153,59 +153,71 @@ type RequiredKeys<T> = { [K in keyof T]-?: object extends Pick<T, K> ? never : K
 type OptionalKeys<T> = { [K in keyof T]-?: object extends Pick<T, K> ? K : never }[keyof T] & string
 
 /**
- * Shape of one {@link WIRE_FRAME_FIELDS} entry, derived from frame interface
- * `T`. Every element of `required` must be one of `T`'s required keys and every
- * element of `optional` one of `T`'s optional keys — so a renamed field, or an
- * optionality flip (`truncated?` → `truncated`, which moves the name between the
- * two arrays' element types), fails typecheck. Completeness in the other
- * direction (every declared key actually appears, and no frame exists on only
- * one side of the wire) is enforced at runtime by the mirror test, which
- * compares these arrays to the Python `TypedDict`'s
- * `__required_keys__`/`__optional_keys__` by exact set equality over the full
- * frame roster.
+ * Whether each key of frame `T` is a `'required'` or `'optional'` wire field.
+ * Because it is `Record<keyof T, …>`, an entry MUST list every key — a field
+ * added to the interface without a corresponding entry fails typecheck — and
+ * `keyof T`-typed keys reject a name no frame declares. The `'required'` /
+ * `'optional'` tag must match the field's actual optionality (checked by
+ * {@link WIRE_FRAME_FIELDS}'s per-entry assertions), so an optionality flip is
+ * caught too. This is the exhaustive counterpart the array form could not
+ * express (a subset array satisfied it silently).
  */
-type FrameFields<T> = {
-  required: readonly RequiredKeys<T>[]
-  optional: readonly OptionalKeys<T>[]
-}
+type FrameFieldRoles<T> = Record<RequiredKeys<T>, 'required'> & Record<OptionalKeys<T>, 'optional'>
 
 /**
- * The wire field names of each frame, split into required and optional keys, as
- * a RUNTIME value the cross-language mirror test asserts `py/protocol.py`'s
- * `TypedDict`s against. The `satisfies` clause binds each entry to its frame
- * interface via {@link FrameFields}, which derives the required/optional key
- * sets FROM the interface — so a renamed, removed, or optionality-flipped field
- * on the TS side fails typecheck, and the mirror test catches a Python-side
- * divergence at runtime. `global` is the JSON key {@link CallMessage} and
- * {@link Namespace} send (a reserved word the Python side carries via a
- * functional `TypedDict`).
+ * Each frame's wire fields tagged by required/optional, keyed by field name so
+ * the mapping is exhaustive over the frame interface (see
+ * {@link FrameFieldRoles}). Bound to the interfaces by `satisfies` below, this
+ * is the single source of truth the cross-language mirror test derives its
+ * expectations from; {@link WIRE_FRAME_FIELDS} projects it to sorted
+ * required/optional arrays for the comparison. `global` is the JSON key
+ * {@link CallMessage} and {@link Namespace} send (a reserved word the Python
+ * side carries via a functional `TypedDict`).
  */
-export const WIRE_FRAME_FIELDS = {
-  BootMessage: { required: ['addressSpaceBytes', 'cpuSeconds', 'maxLogBytes', 'maxValueBytes', 'namespaces', 'type'], optional: [] },
-  Namespace: { required: ['global', 'names'], optional: ['errorClass'] },
-  RunMessage: { required: ['program', 'type'], optional: [] },
-  BootAckMessage: { required: ['type'], optional: [] },
-  CallMessage: { required: ['args', 'global', 'id', 'name', 'type'], optional: [] },
-  LogMessage: { required: ['text', 'type'], optional: ['truncated'] },
-  DoneErrorField: { required: ['kind', 'message'], optional: [] },
-  DoneMessage: { required: ['type'], optional: ['error', 'value'] },
-  ErrorClass: { required: ['memberNameProperty', 'name'], optional: [] },
-  ReplyOk: { required: ['id', 'ok', 'type', 'value'], optional: [] },
-  ReplyErr: { required: ['id', 'message', 'ok', 'type'], optional: [] },
-} satisfies {
-  BootMessage: FrameFields<BootMessage>
-  Namespace: FrameFields<Namespace>
-  RunMessage: FrameFields<RunMessage>
-  BootAckMessage: FrameFields<BootAckMessage>
-  CallMessage: FrameFields<CallMessage>
-  LogMessage: FrameFields<LogMessage>
-  DoneErrorField: FrameFields<DoneErrorField>
-  DoneMessage: FrameFields<DoneMessage>
-  ErrorClass: FrameFields<ErrorClass>
-  ReplyOk: FrameFields<ReplyOk>
-  ReplyErr: FrameFields<ReplyErr>
+const WIRE_FRAME_FIELD_ROLES = {
+  BootMessage: { type: 'required', cpuSeconds: 'required', addressSpaceBytes: 'required', maxLogBytes: 'required', maxValueBytes: 'required', namespaces: 'required' },
+  Namespace: { global: 'required', names: 'required', errorClass: 'optional' },
+  RunMessage: { type: 'required', program: 'required' },
+  BootAckMessage: { type: 'required' },
+  CallMessage: { type: 'required', id: 'required', global: 'required', name: 'required', args: 'required' },
+  LogMessage: { type: 'required', text: 'required', truncated: 'optional' },
+  DoneErrorField: { kind: 'required', message: 'required' },
+  DoneMessage: { type: 'required', value: 'optional', error: 'optional' },
+  ErrorClass: { name: 'required', memberNameProperty: 'required' },
+  ReplyOk: { type: 'required', id: 'required', ok: 'required', value: 'required' },
+  ReplyErr: { type: 'required', id: 'required', ok: 'required', message: 'required' },
+} as const satisfies {
+  BootMessage: FrameFieldRoles<BootMessage>
+  Namespace: FrameFieldRoles<Namespace>
+  RunMessage: FrameFieldRoles<RunMessage>
+  BootAckMessage: FrameFieldRoles<BootAckMessage>
+  CallMessage: FrameFieldRoles<CallMessage>
+  LogMessage: FrameFieldRoles<LogMessage>
+  DoneErrorField: FrameFieldRoles<DoneErrorField>
+  DoneMessage: FrameFieldRoles<DoneMessage>
+  ErrorClass: FrameFieldRoles<ErrorClass>
+  ReplyOk: FrameFieldRoles<ReplyOk>
+  ReplyErr: FrameFieldRoles<ReplyErr>
 }
 
+/**
+ * The wire field names of each frame, split into sorted required and optional
+ * key arrays — the shape the cross-language mirror test compares against
+ * `py/protocol.py`'s `TypedDict` `__required_keys__`/`__optional_keys__`.
+ * Projected from {@link WIRE_FRAME_FIELD_ROLES}, so it inherits that mapping's
+ * exhaustive, optionality-checked binding to the frame interfaces: a TS-side
+ * field add, remove, rename, or optionality flip fails typecheck at the roles
+ * map, and a Python-side divergence fails the mirror test at runtime.
+ */
+export const WIRE_FRAME_FIELDS: Record<keyof typeof WIRE_FRAME_FIELD_ROLES, { required: string[]; optional: string[] }> =
+  Object.fromEntries(
+    Object.entries(WIRE_FRAME_FIELD_ROLES).map(([frame, roles]) => {
+      const required = Object.keys(roles).filter(key => (roles as Record<string, string>)[key] === 'required').sort()
+      const optional = Object.keys(roles).filter(key => (roles as Record<string, string>)[key] === 'optional').sort()
+      return [frame, { required, optional }]
+    }),
+  ) as Record<keyof typeof WIRE_FRAME_FIELD_ROLES, { required: string[]; optional: string[] }>
+
 
 /**
  * The in-band marker text announcing that log capture stopped at the byte