fix(schema-pack): reject YAML block scalars loudly instead of silently truncating manifests (#1750)

Takeover of #1799 (fork branch rotted against master; refreshed CI hit an
unrelated shard-timeout flake). parseYamlMini now throws a clear error when
a bare block-scalar indicator (|, >, |-, >+, |2) reaches parseScalar,
instead of mis-parsing it as the literal string and silently dropping every
following top-level key. Inlines gbrain-recommended.yaml's description so
the bundled pack loads its full page_types.

Co-authored-by: harjoth <harjoth.khara@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-21 16:49:55 -07:00
co-authored by harjoth Claude Fable 5
parent 0612b0daa8
commit 73cd5625d0
3 changed files with 51 additions and 7 deletions
@@ -26,12 +26,7 @@ name: gbrain-recommended
version: 1.0.0
gbrain_min_version: 0.39.0
extends: gbrain-base
description: |
The recommended starter pack for an operational personal brain.
Based on docs/GBRAIN_RECOMMENDED_SCHEMA.md — Karpathy's LLM wiki
pattern extended into a full operational knowledge base. Use this
when you want the documented behavior immediately instead of
authoring your own pack from scratch.
description: "The recommended starter pack for an operational personal brain. Based on docs/GBRAIN_RECOMMENDED_SCHEMA.md — Karpathy's LLM wiki pattern extended into a full operational knowledge base. Use this when you want the documented behavior immediately instead of authoring your own pack from scratch."
# Pack-specific page_types (extend gbrain-base's seed types).
# Each maps a path prefix to a primitive + opt-in flags. Most types
+17 -1
View File
@@ -84,7 +84,9 @@ export function loadPackFromString(content: string, hint: string): SchemaPackMan
*
* Rejected by design: anchors (&), aliases (*), tags (!), flow style
* ({...}, [...] except as JSON), block scalars (|, >), multi-document (---).
* Pack authors who need these features should ship JSON.
* Pack authors who need these features should ship JSON. Block scalars in
* particular now throw a clear error (see parseScalar) rather than silently
* mis-parsing and truncating the rest of the manifest.
*
* This is intentionally narrow. The skill-pack and storage-config parsers
* use similar hand-rolled patterns; this one is shape-customized for pack
@@ -133,6 +135,20 @@ export function parseYamlMini(content: string): unknown {
// Number
if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
if (/^-?\d+\.\d+$/.test(trimmed)) return parseFloat(trimmed);
// Block scalars (`|`, `>`, with optional chomping/indent indicators such
// as `|-`, `>+`, `|2`) are unsupported by design — see the header. A bare
// indicator reaching here is a `key: |` line whose indented body would
// otherwise silently truncate the rest of the mapping: parseScalar would
// return the literal "|"/">", and the over-indented body would trip the
// `indent > baseIndent` break in parseMapping, dropping every following
// top-level key with no diagnostic. Fail loud instead of corrupting.
if (/^[|>][0-9+-]*$/.test(trimmed)) {
throw new Error(
`parseYamlMini: block scalars are not supported (found "${trimmed}"). ` +
`Use an inline or quoted string (e.g. description: "..."), or ship the manifest as JSON. ` +
`Left unhandled, a block scalar silently drops every key after it in the manifest.`,
);
}
// Bare string
return trimmed;
}
+33
View File
@@ -10,6 +10,7 @@
// module's internal contracts.
import { describe, expect, test } from 'bun:test';
import { readFileSync } from 'node:fs';
import { withEnv } from './helpers/with-env.ts';
import {
buildAliasGraph,
@@ -349,6 +350,38 @@ describe('YAML mini-parser', () => {
const result = parseYamlMini('# top comment\nname: value # inline comment') as Record<string, unknown>;
expect(result.name).toBe('value');
});
// Regression: block scalars (`|`, `>`) are unsupported by design. Before
// the fix, `description: |` was read as the literal "|" and its indented
// body silently truncated the mapping — every key after it was dropped
// with no error (issue #1750). Now it fails loud.
test('throws a clear error on block scalars instead of silently truncating', () => {
expect(() => parseYamlMini('description: |\n multi\n line\npage_types:\n - name: deal'))
.toThrow(/block scalars are not supported/);
expect(() => parseYamlMini('description: >\n folded text')).toThrow(/block scalars/);
// Chomping / indent indicators (`|-`, `>+`, `|2`) are caught too.
expect(() => parseYamlMini('x: |-\n body')).toThrow(/block scalars/);
expect(() => parseYamlMini('x: |2\n body')).toThrow(/block scalars/);
});
test('a quoted "|" is still a normal string (not mistaken for a block scalar)', () => {
const result = parseYamlMini('sep: "|"') as Record<string, unknown>;
expect(result.sep).toBe('|');
});
// The bundled gbrain-recommended pack uses a (now inline) description and
// declares 12 page_types. Before #1750 it silently resolved to 0 types.
test('bundled gbrain-recommended.yaml parses with its full page_types', () => {
const yaml = readFileSync(
`${import.meta.dir}/../src/core/schema-pack/base/gbrain-recommended.yaml`,
'utf8',
);
const parsed = parseYamlMini(yaml) as { page_types?: Array<{ name: string }>; description?: string };
expect(Array.isArray(parsed.page_types)).toBe(true);
expect(parsed.page_types!.length).toBeGreaterThanOrEqual(12);
expect(parsed.page_types!.map(t => t.name)).toContain('deal');
expect(parsed.description).not.toBe('|');
});
});
describe('loadPackFromString end-to-end', () => {