cordis-walk.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. /**
  2. * AST helpers shared by the Cordis generators: locate the Cordis module merge
  3. * in a source file and enumerate the `interface Context` keys it declares.
  4. * The vendored core API projector consumes the merge body; the per-subsystem
  5. * region generator's exhaustiveness backstop consumes the key scan.
  6. */
  7. import { globSync, readFileSync } from 'node:fs'
  8. import { resolve, sep } from 'node:path'
  9. import ts from 'typescript'
  10. /** Cheap textual prefilter for a cordis module merge, quote-style agnostic
  11. * (the AST match below reads `stmt.name.text` and never sees the quotes). */
  12. const MERGE_HEAD = /declare module ['"](?:@deepseek-ai\/cordis|\.\/context\.ts)['"]/
  13. /**
  14. * Parse every file matching `patterns` (repo-relative, sorted, `/`-normalized)
  15. * that textually contains a cordis module merge, yielding one entry per merge
  16. * BLOCK — a file may legally hold several `declare module '@deepseek-ai/cordis'` blocks
  17. * (the Typert analyzer reads them all), so the exhaustiveness scan must too.
  18. * Files without a merge are skipped.
  19. * @param scanRoot - Repository root the patterns are resolved against.
  20. * @param patterns - Glob(s) selecting the TypeScript files to scan.
  21. * @returns One entry per cordis module block, in path then source order.
  22. */
  23. export function contextMergeFiles(
  24. scanRoot: string,
  25. patterns: string | readonly string[],
  26. ): { rel: string; sf: ts.SourceFile; text: string; body: ts.ModuleBlock }[] {
  27. const out: { rel: string; sf: ts.SourceFile; text: string; body: ts.ModuleBlock }[] = []
  28. const rels = [...new Set(globSync(patterns as string | string[], { cwd: scanRoot }).map(s => s.split(sep).join('/')))].sort()
  29. for (const rel of rels) {
  30. const abs = resolve(scanRoot, rel)
  31. const text = readFileSync(abs, 'utf8')
  32. if (!MERGE_HEAD.test(text)) continue
  33. const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true)
  34. for (const body of cordisModuleBodies(sf)) out.push({ rel, sf, text, body })
  35. }
  36. return out
  37. }
  38. /** Every cordis module-merge body in `sf`: `declare module '@deepseek-ai/cordis'` (harness
  39. * packages) or `declare module './context.ts'` (vendor core), in source order.
  40. * Module-local: consumers walk blocks through {@link contextMergeFiles}. */
  41. function cordisModuleBodies(sf: ts.SourceFile): ts.ModuleBlock[] {
  42. const bodies: ts.ModuleBlock[] = []
  43. for (const stmt of sf.statements) {
  44. if (!ts.isModuleDeclaration(stmt) || !ts.isStringLiteral(stmt.name)) continue
  45. if (stmt.name.text !== '@deepseek-ai/cordis' && stmt.name.text !== './context.ts') continue
  46. if (stmt.body && ts.isModuleBlock(stmt.body)) bodies.push(stmt.body)
  47. }
  48. return bodies
  49. }
  50. /** The FIRST cordis module-merge body in `sf`, or null without one — for the
  51. * vendor core-API renderer whose input files carry exactly one merge; the
  52. * exhaustiveness scan uses {@link cordisModuleBodies} to read them all. */
  53. export function cordisModuleBody(sf: ts.SourceFile): ts.ModuleBlock | null {
  54. return cordisModuleBodies(sf)[0] ?? null
  55. }
  56. /**
  57. * Every `key: Type` property a `declare module '@deepseek-ai/cordis'` Context merge
  58. * declares in one module body.
  59. * @param body - The cordis module augmentation block.
  60. * @param sf - Owning source file (for text extraction).
  61. * @returns key → declared type-name text, in declaration order.
  62. */
  63. export function contextKeyMap(body: ts.ModuleBlock, sf: ts.SourceFile): Map<string, string> {
  64. const keyToType = new Map<string, string>()
  65. for (const stmt of body.statements) {
  66. if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Context') continue
  67. for (const member of stmt.members) {
  68. if (!ts.isPropertySignature(member) || !member.type) continue
  69. keyToType.set(member.name.getText(sf), member.type.getText(sf))
  70. }
  71. }
  72. return keyToType
  73. }
  74. /**
  75. * Every event name a `declare module '@deepseek-ai/cordis'` Events merge declares in one
  76. * module body. Names are the literal member keys (`'agent/created'`), read
  77. * from method and property members alike so a declaration form the projector
  78. * would reject still enters the exhaustiveness scan.
  79. * @param body - The cordis module augmentation block.
  80. * @param sf - Owning source file (for computed-name text extraction).
  81. * @returns Declared event names, in declaration order.
  82. */
  83. export function eventNameList(body: ts.ModuleBlock, sf: ts.SourceFile): string[] {
  84. const names: string[] = []
  85. for (const stmt of body.statements) {
  86. if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Events') continue
  87. for (const member of stmt.members) {
  88. if (!member.name) continue
  89. names.push(ts.isStringLiteral(member.name) || ts.isIdentifier(member.name)
  90. ? member.name.text
  91. : member.name.getText(sf))
  92. }
  93. }
  94. return names
  95. }