// lint — library.md §9. "Every projection defect so far has been diagnosed by reading daemon logs.
// This makes it a one-liner against a file, offline."
//
// The lint LOGIC lives in lib/ and the CLI is a thin argv shell over it, so the thing worth testing
// is testable and the portable half stays free of Node builtins (§3).
//
// Severity matters and is asserted: an `error` means the record cannot be published (§6.2 fails
// closed), a `warn` means it publishes but loses something, and `info` is a fact worth surfacing.
// Conflating them is how "it lints clean" comes to mean nothing.
import test from 'brittle'
import { lint } from '../lib/lint.js'
const codes = (r) => r.issues.map((i) => i.code).sort()
const bySeverity = (r, s) => r.issues.filter((i) => i.severity === s).map((i) => i.code)
const GOOD = `---
{
"createdAt": "2026-08-05T13:38:23.294Z",
"langs": [
"en"
]
}
---
hello **world** #p2p
`
test('a well-formed record lints clean', (t) => {
const r = lint(GOOD)
t.ok(r.ok, 'ok')
t.alike(bySeverity(r, 'error'), [], 'no errors')
})
test('⛔ malformed frontmatter is an ERROR and blocks publication (§6.2)', (t) => {
const r = lint('---\n{ "createdAt": oops }\n---\n\nbody\n')
t.absent(r.ok)
t.ok(bySeverity(r, 'error').includes('frontmatter-invalid-json'))
t.ok(r.publishable === false, 'and the record is not publishable')
})
test('⛔ a missing createdAt is an ERROR — nothing may be published without it', (t) => {
const r = lint('---\n{}\n---\n\nbody\n')
t.absent(r.ok)
t.ok(bySeverity(r, 'error').includes('no-created-at'))
})
test('an unknown network in reach is an ERROR, not a silent drop', (t) => {
const r = lint('---\n{\n "createdAt": "T",\n "reach": ["bsky", "myspace"]\n}\n---\n\nbody\n')
t.ok(bySeverity(r, 'error').includes('unknown-network'), 'names the network rather than ignoring it')
})
test('reach is reported against the room ceiling only when the room is KNOWN', (t) => {
const src = '---\n{\n "createdAt": "T",\n "reach": ["bsky"]\n}\n---\n\nbody\n'
t.alike(bySeverity(lint(src), 'error'), [], 'without a room, no claim is made — the CLI cannot know')
const withRoom = lint(src, { roomType: 'followers' })
t.ok(bySeverity(withRoom, 'error').includes('reach-exceeds-room'),
'⛔ a followers room can never reach bsky, and saying so is the whole point')
})
test('a spoiler is a WARN — it arrives un-spoilered on bsky (§5, the thesis in one example)', (t) => {
const r = lint('---\n{\n "createdAt": "T"\n}\n---\n\n:::spoiler\nthe ending\n:::\n')
t.ok(bySeverity(r, 'warn').includes('spoiler-revealed-on-bsky'),
'lossless to the fediverse, lossy to bsky, and nothing else tells the author')
t.ok(r.ok, 'a warning does not block publication')
})
test('an unknown directive name is INFO — it still yields its contents (§4.8)', (t) => {
const r = lint('---\n{\n "createdAt": "T"\n}\n---\n\n:::wat\nbody\n:::\n')
t.ok(bySeverity(r, 'info').includes('unknown-directive'))
t.ok(r.ok)
})
test('a bare @mention is INFO — it resolves or is omitted, never guessed (§3.4)', (t) => {
const r = lint('---\n{\n "createdAt": "T"\n}\n---\n\nhi @alice\n')
t.ok(bySeverity(r, 'info').includes('unresolved-mention'))
})
test('a non-http link target is a WARN — the label survives, the link does not (§3.2)', (t) => {
const r = lint('---\n{\n "createdAt": "T"\n}\n---\n\n[x](javascript:alert(1))\n')
t.ok(bySeverity(r, 'warn').includes('link-refused'))
})
test('an empty body with no embed is INFO, not an error (§3.5)', (t) => {
// "A post whose entire source is one image strips to empty text, which is valid."
const r = lint('---\n{\n "createdAt": "T"\n}\n---\n\n\n')
t.ok(r.ok, 'an image-only post is valid')
t.ok(bySeverity(r, 'info').includes('empty-text'))
})
test('lint is TOTAL — never throws', (t) => {
for (const s of ['', '---', '---\n---', 'no frontmatter', '---\n[]\n---\n', null, undefined]) {
try {
const r = lint(s)
t.ok(Array.isArray(r.issues) && typeof r.ok === 'boolean', 'total on ' + JSON.stringify(s))
} catch (err) {
t.fail('threw on ' + JSON.stringify(s) + ': ' + err.message)
}
}
})
test('every issue carries a code, a severity and a spec reference', (t) => {
const r = lint('---\n{ bad }\n---\n\n:::wat\nx\n:::\n\nhi @bob\n')
t.ok(r.issues.length > 0)
for (const i of r.issues) {
t.ok(typeof i.code === 'string' && i.code.length > 0, 'code')
t.ok(['error', 'warn', 'info'].includes(i.severity), 'severity is one of three')
t.ok(typeof i.spec === 'string' && i.spec.length > 0, i.code + ' cites the spec section it enforces')
}
})