session-fixture-layout.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. /** Canonical packed-row and envelope projection helpers for repository session fixtures. */
  2. import { deepStrictEqual } from 'node:assert'
  3. import { execFileSync } from 'node:child_process'
  4. import { existsSync, readFileSync } from 'node:fs'
  5. import { resolve } from 'node:path'
  6. import { packChunkRuns, type SessionEvent } from '@deepseek-ai/dsh-session'
  7. import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
  8. /** One repository session fixture and its canonical projected representation. */
  9. export interface SessionFixtureLayout {
  10. /** Repository-relative path with `/` separators. */
  11. path: string
  12. /** Current fixture bytes decoded as UTF-8. */
  13. source: string
  14. /** Canonical projected fixture bytes. */
  15. canonical: string
  16. }
  17. function isSessionHeader(value: unknown): boolean {
  18. return value !== null && typeof value === 'object' && (value as { type?: unknown }).type === 'session'
  19. }
  20. function renderFixture(headerLine: string, events: readonly SessionEvent[]): string {
  21. return [
  22. headerLine,
  23. ...packChunkRuns(events).map((stored) => {
  24. const record = stored as unknown as Record<string, unknown>
  25. delete record.seq
  26. delete record.time
  27. delete record.seq0
  28. delete record.time0
  29. return JSON.stringify(record)
  30. }),
  31. '',
  32. ].join('\n')
  33. }
  34. function withoutEnvelope(events: readonly SessionEvent[]): Array<Omit<SessionEvent, 'seq' | 'time'>> {
  35. return events.map((event) => {
  36. const { seq: _seq, time: _time, ...projected } = event
  37. return projected
  38. })
  39. }
  40. /**
  41. * Canonicalize one JSONL document when its first record is a session header.
  42. * The header line remains byte-identical; body records decode to logical events,
  43. * re-encode with {@link packChunkRuns}, and omit storage sequence/time envelopes.
  44. * Non-session JSONL returns undefined.
  45. *
  46. * @param content - JSONL source text.
  47. * @param label - path-like diagnostic label.
  48. * @returns Canonical text for a session fixture, otherwise undefined.
  49. */
  50. export function canonicalSessionFixture(content: string, label = '<session-fixture>'): string | undefined {
  51. const headerLine = content.split(/\r?\n/).find(line => line.trim().length > 0)
  52. if (headerLine === undefined) return undefined
  53. let headerValue: unknown
  54. try {
  55. headerValue = JSON.parse(headerLine) as unknown
  56. } catch {
  57. return undefined
  58. }
  59. if (!isSessionHeader(headerValue)) return undefined
  60. let events
  61. try {
  62. events = parseSessionLog(content)
  63. } catch (error) {
  64. const detail = error instanceof Error ? error.message : String(error)
  65. throw new Error(`${label}: ${detail}`, { cause: error })
  66. }
  67. const canonical = renderFixture(headerLine, events)
  68. const decoded = parseSessionLog(canonical)
  69. try {
  70. deepStrictEqual(withoutEnvelope(decoded), withoutEnvelope(events))
  71. } catch (error) {
  72. throw new Error(`${label}: packed snapshot rewrite changed the event payload stream`, { cause: error })
  73. }
  74. if (renderFixture(headerLine, decoded) !== canonical) {
  75. throw new Error(`${label}: packed rewrite is not idempotent`)
  76. }
  77. return canonical
  78. }
  79. /**
  80. * Discover tracked and unignored untracked JSONL files through Git.
  81. *
  82. * @param root - repository root.
  83. * @returns Stable repository-relative paths.
  84. */
  85. function discoverJsonlFiles(root: string): string[] {
  86. return execFileSync(
  87. 'git',
  88. ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '*.jsonl'],
  89. { cwd: root, encoding: 'utf8' },
  90. ).split('\0')
  91. .filter(path => path.length > 0 && existsSync(resolve(root, path)))
  92. .sort()
  93. }
  94. /**
  95. * Inspect every repository JSONL whose first record is a session header.
  96. *
  97. * @param root - repository root.
  98. * @returns Session fixtures with current and canonical text.
  99. */
  100. export function inspectSessionFixtureLayouts(root: string): SessionFixtureLayout[] {
  101. return discoverJsonlFiles(root).flatMap((path) => {
  102. const source = readFileSync(resolve(root, path), 'utf8')
  103. const canonical = canonicalSessionFixture(source, path)
  104. return canonical === undefined ? [] : [{ path, source, canonical }]
  105. })
  106. }