// parse / serialize — spec/record-vocabulary.md §3.2, spec/design.md §5, spec/library.md §6.2, §7, §10.4
//
// The round-trip law `serialize(parse(x)) === x` is LOAD-BEARING, not a nicety (library §10.4):
// `migrateInline` leans on DAG-CBOR being canonical so a rewrite is byte-identical, and ASSERTS it,
// skipping any key whose re-encoded oid differs rather than republishing. Markdown has no such
// guarantee for free — "JSON frontmatter key order, whitespace, trailing newline" must be engineered.
// So every assertion here is on BYTES.
//
// Note what the law does and does not say. §7 states it "for canonical input". serialize() therefore
// emits ONE canonical form, and serialize(parse(x)) NORMALISES non-canonical input rather than
// preserving it. That is the useful direction: a record that has been through the codec once is
// stable forever after, which is what migrateInline needs.
import test from 'brittle'
import { parse, serialize, CANONICAL_KEYS } from '../lib/record.js'
const utf8 = (s) => Buffer.from(s, 'utf8')
const bytes = (t, a, b, m) => t.alike(utf8(a), utf8(b), m)
// The canonical shape: a JSON object in a --- fence, a blank line, the body, one trailing newline.
const CANON = `---
{
"createdAt": "2026-08-05T13:38:23.294Z",
"langs": [
"en"
]
}
---
hello **world**
`
test('parse: the canonical shape', (t) => {
const r = parse(CANON)
t.is(r.frontmatter.createdAt, '2026-08-05T13:38:23.294Z')
t.alike(r.frontmatter.langs, ['en'])
bytes(t, r.body, 'hello **world**\n', 'body is the source below the fence, verbatim')
t.ok(r.publishable, 'well-formed frontmatter is publishable')
t.alike(r.errors, [], 'errors is [] — a claim, not a silence (library §4)')
})
test('round-trip: serialize(parse(x)) === x for canonical input (§7, §10.4)', (t) => {
bytes(t, serialize(parse(CANON)), CANON, 'byte-identical')
})
test('round-trip is IDEMPOTENT — the second pass changes nothing', (t) => {
const once = serialize(parse(CANON))
bytes(t, serialize(parse(once)), once, 'a record through the codec once is stable forever')
})
test('serialize NORMALISES non-canonical input, and then it is stable', (t) => {
// Keys out of order, 4-space indent, no blank line after the fence, no trailing newline.
const messy = '---\n{\n "langs": ["en"],\n "createdAt": "2026-08-05T13:38:23.294Z"\n}\n---\nhello **world**'
const out = serialize(parse(messy))
bytes(t, out, CANON, 'normalised to the canonical form')
bytes(t, serialize(parse(out)), out, 'and stable thereafter')
})
test('canonical key ORDER is pinned, not insertion order (§10.4)', (t) => {
const a = serialize({ frontmatter: { langs: ['en'], createdAt: 'T' }, body: 'x\n' })
const b = serialize({ frontmatter: { createdAt: 'T', langs: ['en'] }, body: 'x\n' })
bytes(t, a, b, 'two key orders serialise identically')
t.is(CANONICAL_KEYS[0], 'createdAt', 'createdAt leads the canonical order')
})
test('unknown keys survive, ordered after the pinned ones', (t) => {
const src = serialize({
frontmatter: { 'net.favela.ext.zzz': 1, createdAt: 'T', 'net.favela.ext.aaa': 2 },
body: 'x\n'
})
const back = parse(src)
t.is(back.frontmatter['net.favela.ext.aaa'], 2, 'unknown keys are preserved — §1.1 extensibility')
t.ok(src.indexOf('"createdAt"') < src.indexOf('"net.favela.ext.aaa"'), 'pinned keys lead')
t.ok(src.indexOf('"net.favela.ext.aaa"') < src.indexOf('"net.favela.ext.zzz"'), 'then sorted')
bytes(t, serialize(parse(src)), src, 'and it round-trips')
})
test('⛔ malformed frontmatter is readable but NOT publishable (§6.2)', (t) => {
// JSON-in-a-fence was chosen over YAML because `audience: no` parsing as false is a privacy bug.
// The remaining gap was that frontmatter parsing had no totality rule. This is it.
const broken = '---\n{ "createdAt": oops }\n---\n\nthe body still reads\n'
const r = parse(broken)
t.absent(r.publishable, 'fails CLOSED — degrades to local-only, never to Public')
t.ok(r.errors.length > 0, 'and says why')
bytes(t, r.body, 'the body still reads\n', 'still readable and renderable')
})
test('no frontmatter at all: readable, not publishable', (t) => {
const r = parse('just a body\n')
t.absent(r.publishable, 'nothing asserts createdAt, so nothing may be published')
bytes(t, r.body, 'just a body\n', 'the whole source is the body')
})
test('parse is TOTAL — never throws (§2 applies to the whole codec)', (t) => {
const hostile = ['', '---', '---\n', '---\n---', '---\n{}\n---', '---\nnot json\n---\nbody',
'\n\n\n', '---\n{}\n---\n\n\n', '{ "createdAt": "T" }']
for (const s of hostile) {
try {
const r = parse(s)
t.ok(typeof r.body === 'string' && typeof r.publishable === 'boolean', 'total on ' + JSON.stringify(s))
} catch (err) {
t.fail('threw on ' + JSON.stringify(s) + ': ' + err.message)
}
}
})
test('a body containing a --- fence is not truncated', (t) => {
const src = '---\n{\n "createdAt": "T"\n}\n---\n\nabove\n\n---\n\nbelow\n'
const r = parse(src)
t.ok(r.body.includes('below'), 'only the FIRST fence pair delimits the frontmatter')
bytes(t, serialize(parse(src)), src, 'and it round-trips')
})