config.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. /**
  2. * Configuration normalization for workspace instruction discovery and rendering.
  3. *
  4. * @module @deepseek-ai/dsh-workspace-context/config
  5. */
  6. import z from 'schemastery'
  7. import { resolveDshHome } from '@deepseek-ai/dsh-paths'
  8. const DEFAULT_PROJECT_ROOT_MARKERS = ['.git'] as const
  9. const DEFAULT_INSTRUCTION_FILE_CANDIDATES = ['AGENTS.md', 'CLAUDE.md'] as const
  10. const DEFAULT_LOCAL_INSTRUCTION_FILE_CANDIDATES = ['AGENTS.local.md', 'CLAUDE.local.md'] as const
  11. const DEFAULT_MAX_SOURCE_BYTES = 1_048_576
  12. const RESERVED_PATH_SEGMENTS = new Set(['', '.', '..'])
  13. /** User-facing workspace instruction loader configuration. */
  14. export interface Config {
  15. /** Harness home containing the fixed user-global `AGENTS.md`; defaults to `$DSH_HOME` or `~/.dsh`. */
  16. dshHome?: string
  17. /** Directory entries that identify the project root while walking upward from the session cwd. */
  18. projectRootMarkers?: string[]
  19. /** UTF-8 byte cap for one rendered baseline or dynamic batch; non-positive or non-finite disables loading. */
  20. maxBytes: number
  21. /** Maximum UTF-8 bytes read from one instruction file; larger files are ignored. */
  22. maxSourceBytes?: number
  23. /**
  24. * Ordered same-directory project candidates; every existing file loads, with
  25. * per-directory trimmed-content duplicates collapsed to the earliest candidate.
  26. */
  27. instructionFileCandidates?: string[]
  28. /**
  29. * Ordered same-directory local-overlay candidates loaded after the base files
  30. * under the same per-directory trimmed-content dedup; empty disables the overlay.
  31. */
  32. localInstructionFileCandidates?: string[]
  33. }
  34. export const Config: z<Config> = z.object({
  35. dshHome: z.string(),
  36. projectRootMarkers: z.array(z.string()).default([...DEFAULT_PROJECT_ROOT_MARKERS]),
  37. maxBytes: z.number().required(),
  38. maxSourceBytes: z.number().step(1).min(1).default(DEFAULT_MAX_SOURCE_BYTES),
  39. instructionFileCandidates: z.array(z.string()).default([...DEFAULT_INSTRUCTION_FILE_CANDIDATES]),
  40. localInstructionFileCandidates: z.array(z.string()).default([...DEFAULT_LOCAL_INSTRUCTION_FILE_CANDIDATES]),
  41. })
  42. /** Normalized instruction discovery configuration. */
  43. export interface ResolvedDiscoveryConfig {
  44. dshHome: string
  45. projectRootMarkers: string[]
  46. instructionFileCandidates: string[]
  47. localInstructionFileCandidates: string[]
  48. }
  49. /** Normalized configuration used by discovery and reconciliation. */
  50. export interface ResolvedConfig extends ResolvedDiscoveryConfig {
  51. maxBytes: number
  52. maxSourceBytes: number
  53. }
  54. /**
  55. * Resolve defaults, the harness home, and valid same-directory candidates.
  56. * @param config - user-facing plugin configuration.
  57. * @returns normalized runtime configuration.
  58. */
  59. export function resolveConfig(config: Config): ResolvedConfig {
  60. return {
  61. ...resolveDiscoveryConfig(config),
  62. maxBytes: config.maxBytes,
  63. maxSourceBytes: config.maxSourceBytes ?? DEFAULT_MAX_SOURCE_BYTES,
  64. }
  65. }
  66. /**
  67. * Resolve the subset of configuration used before instruction content is rendered.
  68. * @param config - optional discovery controls.
  69. * @returns normalized home, root markers, and instruction candidates.
  70. */
  71. export function resolveDiscoveryConfig(
  72. config: Pick<Config, 'dshHome' | 'projectRootMarkers' | 'instructionFileCandidates' | 'localInstructionFileCandidates'>,
  73. ): ResolvedDiscoveryConfig {
  74. return {
  75. dshHome: resolveDshHome(config.dshHome),
  76. projectRootMarkers: config.projectRootMarkers ?? [...DEFAULT_PROJECT_ROOT_MARKERS],
  77. instructionFileCandidates: resolveInstructionFileCandidates(
  78. config.instructionFileCandidates,
  79. DEFAULT_INSTRUCTION_FILE_CANDIDATES,
  80. ),
  81. localInstructionFileCandidates: resolveInstructionFileCandidates(
  82. config.localInstructionFileCandidates,
  83. DEFAULT_LOCAL_INSTRUCTION_FILE_CANDIDATES,
  84. ),
  85. }
  86. }
  87. function resolveInstructionFileCandidates(candidates: string[] | undefined, fallback: readonly string[]): string[] {
  88. return (candidates ?? [...fallback]).filter(candidate => (
  89. !RESERVED_PATH_SEGMENTS.has(candidate) && !/[\\/]/.test(candidate)
  90. ))
  91. }