(projectRoot: string)
| 149 | * @returns Parsed config or null if file doesn't exist |
| 150 | */ |
| 151 | export function readProjectConfig(projectRoot: string): ProjectConfig | null { |
| 152 | const configPath = resolveConfigFilePath(projectRoot); |
| 153 | if (configPath === null) { |
| 154 | return null; // No config is OK |
| 155 | } |
| 156 | |
| 157 | try { |
| 158 | const content = readFileSync(configPath, 'utf-8'); |
| 159 | const raw = parseYaml(content); |
| 160 | |
| 161 | if (!raw || typeof raw !== 'object') { |
| 162 | console.warn(`openspec/config.yaml is not a valid YAML object`); |
| 163 | return null; |
| 164 | } |
| 165 | |
| 166 | const config: Partial<ProjectConfig> = {}; |
| 167 | |
| 168 | // Parse schema field using Zod |
| 169 | const schemaField = z.string().min(1); |
| 170 | const schemaResult = schemaField.safeParse(raw.schema); |
| 171 | if (schemaResult.success) { |
| 172 | config.schema = schemaResult.data; |
| 173 | } else if (raw.schema !== undefined) { |
| 174 | console.warn(`Invalid 'schema' field in config (must be non-empty string)`); |
| 175 | } |
| 176 | |
| 177 | // Parse context field with size limit |
| 178 | if (raw.context !== undefined) { |
| 179 | const contextField = z.string(); |
| 180 | const contextResult = contextField.safeParse(raw.context); |
| 181 | |
| 182 | if (contextResult.success) { |
| 183 | const contextSize = Buffer.byteLength(contextResult.data, 'utf-8'); |
| 184 | if (contextSize > MAX_CONTEXT_SIZE) { |
| 185 | console.warn( |
| 186 | `Context too large (${(contextSize / 1024).toFixed(1)}KB, limit: ${MAX_CONTEXT_SIZE / 1024}KB)` |
| 187 | ); |
| 188 | console.warn(`Ignoring context field`); |
| 189 | } else { |
| 190 | config.context = contextResult.data; |
| 191 | } |
| 192 | } else { |
| 193 | console.warn(`Invalid 'context' field in config (must be string)`); |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | // Parse rules field using Zod |
| 198 | if (raw.rules !== undefined) { |
| 199 | const rulesField = z.record(z.string(), z.array(z.string())); |
| 200 | |
| 201 | // First check if it's an object structure (guard against null since typeof null === 'object') |
| 202 | if (typeof raw.rules === 'object' && raw.rules !== null && !Array.isArray(raw.rules)) { |
| 203 | const parsedRules: Record<string, string[]> = {}; |
| 204 | let hasValidRules = false; |
| 205 | |
| 206 | for (const [artifactId, rules] of Object.entries(raw.rules)) { |
| 207 | const rulesArrayResult = z.array(z.string()).safeParse(rules); |
| 208 |
no test coverage detected