fix(tools): amortize class-name allocation and tighten py-types render contract

Address ds-review-bot v5/v6 review round 5:
- allocateClassName: keep a per-base collision counter (state.nextClassCounter)
  so a deep single-field chain sharing one capped base allocates in amortized
  O(1) instead of rescanning from 2 each time (Theta(depth^2) time); remove the
  stale one-line JSDoc left above the multiline one and attach the doc to the
  function, not the constant.
- renderType's catch rolls back the typing symbols the discarded subtree added
  (not just the classes) so the import line still lists exactly the symbols the
  surviving output uses; the comment now names that the same path also degrades
  this module's internal-invariant throws to Any, the trade for never throwing.
- README (both languages) no longer describes an installable
  dsh-code-runtime-python package: the Python renderer is built in and drives
  any runtime reporting language: 'python'; the first-party backend ships
  separately.
- Tests: assert the render-phase degrade on the first call, assert the import
  line after rollback, and cover the collision-skip loop; py-types.ts stays at
  100% per-file coverage.
This commit is contained in:
Chinesezjc
2026-08-02 14:50:01 +08:00
parent d7b4b014eb
commit 1614f19686
5 changed files with 69 additions and 19 deletions

View File

@@ -54,7 +54,10 @@ describe('jsonSchemaToPy', () => {
it('degrades to Any when a stateful getter throws in the render phase after passing validation', () => {
// A hostile `type` getter returns a scalar on the validation read, then
// throws on the render read. The no-throw contract must still hold across
// the whole walk, degrading the node to Any rather than escaping.
// the whole walk, degrading the node to Any rather than escaping. Assert
// the FIRST call's result: within it, root validation reads `type` once
// and the render phase reads it again (the throw), so this exercises the
// render-phase catch, not the validation-catch path.
let reads = 0
const schema = {
get type() {
@@ -63,8 +66,9 @@ describe('jsonSchemaToPy', () => {
throw new Error('stateful getter')
},
}
expect(() => jsonSchemaToPy(schema)).not.toThrow()
expect(jsonSchemaToPy(schema)).toBe('Any')
let first: string | undefined
expect(() => { first = jsonSchemaToPy(schema) }).not.toThrow()
expect(first).toBe('Any')
})
it('rolls back partial class declarations when a nested render-phase throw degrades a tool', () => {
@@ -88,6 +92,10 @@ describe('jsonSchemaToPy', () => {
// entire renderType call); no partial TypedDict for it is declared.
expect(text).toContain('async def hostile(self, args: Any) -> str: ...')
expect(text).not.toContain('class HostileArgs(TypedDict):')
// The import line lists only symbols the surviving output uses: the
// discarded subtree's TypedDict/NotRequired must not leak into it.
expect(text).not.toContain('TypedDict')
expect(text).toContain('from typing import Any, Protocol')
})
it('keeps class names and total output linear for a deep single-field object chain', () => {
@@ -113,6 +121,29 @@ describe('jsonSchemaToPy', () => {
expect(text.length).toBeLessThan(depth * 400)
})
it('skips an already-taken counter suffix when a sibling object occupies it', () => {
// `phase` and `Phase` both CamelCase to the base `FooArgsPhase`; `phase2`
// independently allocates `FooArgsPhase2` first. When `Phase` collides, the
// counter's first candidate `FooArgsPhase2` is already taken, so the scan
// must advance to `FooArgsPhase3` (exercises the collision-skip loop).
const obj = (field: string) => ({ type: 'object' as const, additionalProperties: false, properties: { [field]: { type: 'string' } } })
const tool: ToolSdkSchema = {
name: 'foo',
description: 'Sibling objects with colliding class bases.',
parameters: {
type: 'object',
additionalProperties: false,
properties: { phase: obj('a'), phase2: obj('b'), Phase: obj('c') },
required: ['phase', 'phase2', 'Phase'],
},
output: { type: 'string' },
}
const text = renderToolsSdkPy([tool])
expect(text).toContain('class FooArgsPhase(TypedDict):')
expect(text).toContain('class FooArgsPhase2(TypedDict):')
expect(text).toContain('class FooArgsPhase3(TypedDict):')
})
it('emits exact digits for a beyond-safe-range integer literal', () => {
// Python integers are arbitrary-precision, so the emitted digits ARE the
// value the model programs against. `String(2 ** 60)` prints the rounded