fix(gsd): parse raw YAML under preference headings (#2794)
Accept raw YAML blocks beneath markdown preference headings while preserving legacy heading-list parsing. Closes #2787
This commit is contained in:
parent
c88a9d2d4f
commit
6918fb76c6
2 changed files with 66 additions and 14 deletions
|
|
@ -22,6 +22,7 @@ import { normalizeStringArray } from "../shared/format-utils.js";
|
|||
import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";
|
||||
|
||||
import {
|
||||
KNOWN_PREFERENCE_KEYS,
|
||||
MODE_DEFAULTS,
|
||||
type WorkflowMode,
|
||||
type GSDPreferences,
|
||||
|
|
@ -250,7 +251,7 @@ function parseFrontmatterBlock(frontmatter: string): GSDPreferences {
|
|||
* - planner: sonnet
|
||||
*/
|
||||
function parseHeadingListFormat(content: string): GSDPreferences {
|
||||
const result: Record<string, Record<string, string>> = {};
|
||||
const result: Record<string, string[]> = {};
|
||||
let currentSection: string | null = null;
|
||||
|
||||
for (const rawLine of content.split('\n')) {
|
||||
|
|
@ -258,27 +259,44 @@ function parseHeadingListFormat(content: string): GSDPreferences {
|
|||
const headingMatch = line.match(/^##\s+(.+)$/);
|
||||
if (headingMatch) {
|
||||
currentSection = headingMatch[1].trim().toLowerCase().replace(/\s+/g, '_');
|
||||
if (!result[currentSection]) result[currentSection] = [];
|
||||
continue;
|
||||
}
|
||||
if (currentSection) {
|
||||
const itemMatch = line.match(/^-\s+([^:]+):\s*(.*)$/);
|
||||
if (itemMatch) {
|
||||
if (!result[currentSection]) result[currentSection] = {};
|
||||
const value = itemMatch[2].trim();
|
||||
// Coerce "true"/"false" strings and numbers
|
||||
result[currentSection][itemMatch[1].trim()] = value;
|
||||
}
|
||||
if (currentSection && line.trim() && !line.trimStart().startsWith('#')) {
|
||||
result[currentSection].push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert string values to appropriate types via YAML parser for each section
|
||||
const typed: Record<string, unknown> = {};
|
||||
for (const [section, entries] of Object.entries(result)) {
|
||||
const yamlLines = Object.entries(entries).map(([k, v]) => `${k}: ${v}`).join('\n');
|
||||
for (const [section, lines] of Object.entries(result)) {
|
||||
if (lines.length === 0) continue;
|
||||
|
||||
const usesLegacyListItems = lines.every((line) => /^\s*-\s+[^:]+:\s*.*$/.test(line));
|
||||
const yamlBlock = usesLegacyListItems
|
||||
? lines.map((line) => line.replace(/^\s*-\s+/, '')).join('\n')
|
||||
: lines.join('\n');
|
||||
|
||||
try {
|
||||
typed[section] = parseYaml(yamlLines);
|
||||
const parsed = parseYaml(yamlBlock);
|
||||
if (typeof parsed !== 'object' || parsed === null) continue;
|
||||
|
||||
let targetSection = section;
|
||||
let value: unknown = parsed;
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
const keys = Object.keys(parsed);
|
||||
if (keys.length === 1) {
|
||||
const [onlyKey] = keys;
|
||||
if (onlyKey === section || (!KNOWN_PREFERENCE_KEYS.has(section) && KNOWN_PREFERENCE_KEYS.has(onlyKey))) {
|
||||
targetSection = onlyKey;
|
||||
value = (parsed as Record<string, unknown>)[onlyKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typed[targetSection] = value;
|
||||
} catch {
|
||||
typed[section] = entries;
|
||||
/* malformed section — skip */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -352,6 +352,40 @@ test("handles empty models config", () => {
|
|||
assert.equal(prefs!.models, undefined);
|
||||
});
|
||||
|
||||
test("parses raw YAML blocks under headings", () => {
|
||||
const content = `## Parallel
|
||||
enabled: true
|
||||
max_workers: 3
|
||||
`;
|
||||
const prefs = parsePreferencesMarkdown(content);
|
||||
assert.notEqual(prefs, null);
|
||||
assert.equal(prefs!.parallel?.enabled, true);
|
||||
assert.equal(prefs!.parallel?.max_workers, 3);
|
||||
});
|
||||
|
||||
test("unwraps nested top-level preference key under descriptive headings", () => {
|
||||
const content = `## Parallel Orchestration
|
||||
parallel:
|
||||
enabled: true
|
||||
max_workers: 3
|
||||
`;
|
||||
const prefs = parsePreferencesMarkdown(content);
|
||||
assert.notEqual(prefs, null);
|
||||
assert.equal(prefs!.parallel?.enabled, true);
|
||||
assert.equal(prefs!.parallel?.max_workers, 3);
|
||||
});
|
||||
|
||||
test("preserves legacy heading list format", () => {
|
||||
const content = `## Git
|
||||
- isolation: branch
|
||||
- auto_push: true
|
||||
`;
|
||||
const prefs = parsePreferencesMarkdown(content);
|
||||
assert.notEqual(prefs, null);
|
||||
assert.equal(prefs!.git?.isolation, "branch");
|
||||
assert.equal(prefs!.git?.auto_push, true);
|
||||
});
|
||||
|
||||
// ── Warn-once for unrecognized format (#2373) ────────────────────────────────
|
||||
|
||||
test("unrecognized format warning is emitted at most once (#2373)", () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue