v0.37.9.0 fix(frontmatter): canonical-style normalization for tag arrays (#1252)

Aligns the auto-fix engine, the inferred-frontmatter serializer, and the
agent-facing skill on a single canonical YAML shape for tag arrays. v0.37.5.0
fixed the validator (it stopped flagging valid YAML); this release lines up
everything else with that fix.

Layer 1 (brain-writer.ts step 3a): allow-listed to `tags:` / `aliases:` keys.
Rewrites `tags: ["yc"]` to `tags: ['yc']`; apostrophe fallback for
`"Men's Fashion"`. Shares a NESTED_QUOTES dedup gate with the existing
step 3 so one file with both rewrites surfaces as one audit entry, not two.

Layer 4 (frontmatter-inference.ts): serializer emits the same canonical
single-quote form by default. Inferred frontmatter on import and `--fix`
output now match byte-for-byte.

Layer 5 (frontmatter-guard SKILL.md): new "Prevention" section showing
canonical vs JSON-style arrays + the JSON.stringify trap that produces
the non-canonical form. Future agent writes start canonical.

Parity test added to markdown-validation.test.ts pinning agreement between
per-value safeLoad parsing and gray-matter full-document parse on the
load-bearing inputs.

PR #1238's "Layer 3" (put_page auto-normalization) was dropped during
plan review: put_page parses YAML into typed fields and hashes them, so
single-quoted vs double-quoted arrays are functionally identical in
storage. The fix lives where the writes happen, not on the read path.

Source PRs absorbed: #1217 (closed, serializer fix) + #1238 (closed,
four-layer defense-in-depth narrowed to three layers). PR #1229 already
merged as v0.37.5.0.

Co-authored-by: garrytan-agents <garrytan-agents@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-21 09:26:37 -07:00
committed by GitHub
co-authored by garrytan-agents Claude Opus 4.7
parent 54a0629745
commit f2e11d6967
10 changed files with 293 additions and 10 deletions
+31
View File
@@ -177,6 +177,37 @@ describe('parseMarkdown validation surface', () => {
);
expect(broken.length).toBeGreaterThan(0);
});
// v0.37.9.0 — parity test (codex outside-voice review D7-3).
// The validator parses ONLY the value with safeLoad. Gray-matter parses
// the whole frontmatter document. These two can disagree on edge cases
// (e.g. a value valid in isolation but ambiguous in document context).
// For the load-bearing inputs this wave targets, both paths must agree:
// valid YAML doesn't trigger NESTED_QUOTES, and clearly broken YAML
// either triggers NESTED_QUOTES or YAML_PARSE (never silent).
test('parity: validator per-value safeLoad agrees with gray-matter full-document parse', () => {
const cases: { md: string; shouldFlag: boolean; label: string }[] = [
// Valid: gray-matter parses cleanly, validator should NOT flag.
{ md: `${fence}\ntype: concept\ntags: ["yc", "w2025"]\n${fence}\n\nbody`, shouldFlag: false, label: 'JSON-style array (valid YAML)' },
{ md: `${fence}\ntype: concept\ntags: ['yc', 'w2025']\n${fence}\n\nbody`, shouldFlag: false, label: 'single-quoted array' },
{ md: `${fence}\ntype: concept\ntitle: 'a: "b" "c"'\n${fence}\n\nbody`, shouldFlag: false, label: 'single-quoted scalar with literal inner quotes' },
{ md: `${fence}\ntype: concept\ntitle: ok\n${fence}\n\nbody`, shouldFlag: false, label: 'clean scalar' },
// Broken: gray-matter would fail OR produce ambiguous parse, validator
// should surface either NESTED_QUOTES or YAML_PARSE.
{ md: `${fence}\ntype: concept\ntitle: "Foo "bar" baz "qux" end"\n${fence}\n\nbody`, shouldFlag: true, label: 'nested scalar quotes' },
];
for (const c of cases) {
const parsed = parseMarkdown(c.md, undefined, { validate: true });
const errors = parsed.errors!.filter(
e => e.code === 'NESTED_QUOTES' || e.code === 'YAML_PARSE'
);
if (c.shouldFlag) {
expect(errors.length, `[${c.label}] expected at least one NESTED_QUOTES or YAML_PARSE error`).toBeGreaterThan(0);
} else {
expect(errors.length, `[${c.label}] expected no NESTED_QUOTES/YAML_PARSE errors but got ${JSON.stringify(errors)}`).toBe(0);
}
}
});
});
describe('EMPTY_FRONTMATTER', () => {