diff --git a/src/core/project-config.ts b/src/core/project-config.ts index 678dc4e5..45748820 100644 --- a/src/core/project-config.ts +++ b/src/core/project-config.ts @@ -117,8 +117,8 @@ export function readProjectConfig(projectRoot: string): ProjectConfig | null { if (raw.rules !== undefined) { const rulesField = z.record(z.string(), z.array(z.string())); - // First check if it's an object structure - if (typeof raw.rules === 'object' && !Array.isArray(raw.rules)) { + // First check if it's an object structure (guard against null since typeof null === 'object') + if (typeof raw.rules === 'object' && raw.rules !== null && !Array.isArray(raw.rules)) { const parsedRules: Record = {}; let hasValidRules = false; diff --git a/test/core/project-config.test.ts b/test/core/project-config.test.ts index 35710c65..abfd2a38 100644 --- a/test/core/project-config.test.ts +++ b/test/core/project-config.test.ts @@ -142,6 +142,30 @@ rules: ["not", "an", "object"] ); }); + it('should handle rules: null without aborting config parsing', () => { + // YAML `rules:` with no value parses to null + const configDir = path.join(tempDir, 'openspec'); + fs.mkdirSync(configDir, { recursive: true }); + fs.writeFileSync( + path.join(configDir, 'config.yaml'), + `schema: spec-driven +context: Valid context +rules: +` + ); + + const config = readProjectConfig(tempDir); + + // Should still parse schema and context despite null rules + expect(config).toEqual({ + schema: 'spec-driven', + context: 'Valid context', + }); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining("Invalid 'rules' field") + ); + }); + it('should filter out invalid rules for specific artifact', () => { const configDir = path.join(tempDir, 'openspec'); fs.mkdirSync(configDir, { recursive: true });