config.ts 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  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_MAX_SOURCE_BYTES = 1_048_576
  11. const RESERVED_PATH_SEGMENTS = new Set(['', '.', '..'])
  12. /** User-facing workspace instruction loader configuration. */
  13. export interface Config {
  14. /** Harness home containing the fixed user-global `AGENTS.md`; defaults to `$DSH_HOME` or `~/.dsh`. */
  15. dshHome?: string
  16. /** Directory entries that identify the project root while walking upward from the session cwd. */
  17. projectRootMarkers?: string[]
  18. /** UTF-8 byte cap for one rendered baseline or dynamic batch; non-positive or non-finite disables loading. */
  19. maxBytes: number
  20. /** Maximum UTF-8 bytes read from one instruction file; larger files are ignored. */
  21. maxSourceBytes?: number
  22. /** Ordered same-directory project candidates; the first existing regular file wins in each scope. */
  23. instructionFileCandidates?: string[]
  24. }
  25. export const Config: z<Config> = z.object({
  26. dshHome: z.string(),
  27. projectRootMarkers: z.array(z.string()).default([...DEFAULT_PROJECT_ROOT_MARKERS]),
  28. maxBytes: z.number().required(),
  29. maxSourceBytes: z.number().step(1).min(1).default(DEFAULT_MAX_SOURCE_BYTES),
  30. instructionFileCandidates: z.array(z.string()).default([...DEFAULT_INSTRUCTION_FILE_CANDIDATES]),
  31. })
  32. /** Normalized instruction discovery configuration. */
  33. export interface ResolvedDiscoveryConfig {
  34. dshHome: string
  35. projectRootMarkers: string[]
  36. instructionFileCandidates: string[]
  37. }
  38. /** Normalized configuration used by discovery and reconciliation. */
  39. export interface ResolvedConfig extends ResolvedDiscoveryConfig {
  40. maxBytes: number
  41. maxSourceBytes: number
  42. }
  43. /**
  44. * Resolve defaults, the harness home, and valid same-directory candidates.
  45. * @param config - user-facing plugin configuration.
  46. * @returns normalized runtime configuration.
  47. */
  48. export function resolveConfig(config: Config): ResolvedConfig {
  49. return {
  50. ...resolveDiscoveryConfig(config),
  51. maxBytes: config.maxBytes,
  52. maxSourceBytes: config.maxSourceBytes ?? DEFAULT_MAX_SOURCE_BYTES,
  53. }
  54. }
  55. /**
  56. * Resolve the subset of configuration used before instruction content is rendered.
  57. * @param config - optional discovery controls.
  58. * @returns normalized home, root markers, and instruction candidates.
  59. */
  60. export function resolveDiscoveryConfig(
  61. config: Pick<Config, 'dshHome' | 'projectRootMarkers' | 'instructionFileCandidates'>,
  62. ): ResolvedDiscoveryConfig {
  63. return {
  64. dshHome: resolveDshHome(config.dshHome),
  65. projectRootMarkers: config.projectRootMarkers ?? [...DEFAULT_PROJECT_ROOT_MARKERS],
  66. instructionFileCandidates: resolveInstructionFileCandidates(config.instructionFileCandidates),
  67. }
  68. }
  69. function resolveInstructionFileCandidates(candidates: string[] | undefined): string[] {
  70. return (candidates ?? [...DEFAULT_INSTRUCTION_FILE_CANDIDATES]).filter(candidate => (
  71. !RESERVED_PATH_SEGMENTS.has(candidate) && !/[\\/]/.test(candidate)
  72. ))
  73. }