fix(sdk): anchor extractFrontmatter at file start (#3240) (#3247)

* test: reproduce extractFrontmatter LAST-block bug (#3240)

* fix(sdk): anchor extractFrontmatter at file start (#3240)

* changeset: pr=3247 for #3240
This commit is contained in:
Tom Boucher
2026-05-08 08:40:30 -04:00
committed by GitHub
parent 447763411a
commit b2f0fdf250
5 changed files with 122 additions and 12 deletions

View File

@@ -66,10 +66,62 @@ describe('extractFrontmatter', () => {
expect(result).toEqual({ items: ['one', 'two'] });
});
it('uses the LAST block when multiple stacked blocks exist', () => {
it('uses the LEADING block when multiple stacked blocks exist', () => {
// extractFrontmatter is anchored at file start — leading block wins (#3240 fix)
const content = '---\nold: data\n---\n---\nnew: data\n---\nbody';
const result = extractFrontmatter(content);
expect(result).toEqual({ new: 'data' });
expect(result).toEqual({ old: 'data' });
});
it('returns the LEADING block when body contains markdown horizontal rules', () => {
// Regression: LAST-block semantics picked up body separators as frontmatter (#3240)
const content = [
'---',
'wave: 3',
'autonomous: false',
'phase: 05-hardening',
'---',
'',
'## Section One',
'',
'---',
'',
'## Section Two',
'',
'---',
'',
'body text',
].join('\n');
const result = extractFrontmatter(content);
expect(result.wave).toBe('3');
expect(result.autonomous).toBe('false');
expect(result.phase).toBe('05-hardening');
});
it('returns the LEADING block when body contains embedded YAML in fenced code block', () => {
// Regression: LAST-block semantics matched YAML inside ```yaml fences (#3240)
const content = [
'---',
'wave: 2',
'autonomous: true',
'phase: 04-polish',
'---',
'',
'## Example',
'',
'```yaml',
'---',
'name: example',
'value: 99',
'---',
'```',
'',
'More body text.',
].join('\n');
const result = extractFrontmatter(content);
expect(result.wave).toBe('2');
expect(result.autonomous).toBe('true');
expect(result.phase).toBe('04-polish');
});
it('handles empty-object-to-array conversion', () => {

View File

@@ -174,20 +174,18 @@ export function extractFrontmatterLeading(content: string): Record<string, unkno
* - Nested objects via indentation
* - Inline arrays: key: [a, b, c]
* - Dash arrays with auto-conversion from empty objects
* - Multiple stacked blocks (uses the LAST match)
* - CRLF line endings
* - Quoted value stripping
*
* Anchored at the start of the file — only the leading `---...---` block is
* considered canonical frontmatter. Body `---` separators and embedded YAML
* examples inside fenced code blocks are never picked up.
*
* @param content - File content potentially containing frontmatter
* @returns Parsed frontmatter as a record, or empty object if none found
*/
export function extractFrontmatter(content: string): Record<string, unknown> {
// Find ALL frontmatter blocks. Use the LAST one (corruption recovery).
const allBlocks = [...content.matchAll(/(?:^|\n)\s*---\r?\n([\s\S]+?)\r?\n---/g)];
const match = allBlocks.length > 0 ? allBlocks[allBlocks.length - 1] : null;
if (!match) return {};
return parseFrontmatterYamlLines(match[1]);
return extractFrontmatterLeading(content);
}
// ─── stripFrontmatter ───────────────────────────────────────────────────────