Match bDS2 canonical file serialization

This commit is contained in:
2026-07-22 20:19:21 +02:00
parent fdf47da40a
commit 7b8e539340
17 changed files with 706 additions and 338 deletions

View File

@@ -364,7 +364,10 @@ invariant MediaSidecarFormat {
-- Sidecar files use YAML-like key-value format (hand-built, not gray-matter)
-- Path: {binary_path}.meta
-- Only truthy fields are written (except required fields)
-- Fields: title, alt, caption, author, tags, language, linkedPostIds
-- linkedPostIds is always written (including []), immediately before tags.
-- Always-quoted values and inline-list strings escape backslash, quote, and
-- newline; the reader reverses those escapes.
-- Fields include: title, alt, caption, author, language, linkedPostIds, tags.
-- Note: 'filename' is NOT written to sidecar (it is the binary filename itself)
}

View File

@@ -81,6 +81,10 @@ invariant MenuPersistedAsOpml {
-- OPML outline attributes: text (label), type (kind),
-- pageSlug (slug for page/home), categoryName (slug for category_archive).
-- Nested <outline> elements represent submenu children.
-- The head title is the project name. dateCreated and dateModified both use
-- the project's updated timestamp, falling back to created timestamp.
-- Empty outlines have one space before '/>' and the document ends in one
-- newline, matching bDS2's canonical serializer.
parse_opml(read_file("meta/menu.opml")) = menu.items
}

View File

@@ -70,6 +70,17 @@ invariant MetadataPersistedAsFiles {
-- meta/category-meta.json — per-category render settings
-- meta/publishing.json — SSH connection details (non-secret)
-- All writes are atomic (temp file + rename)
-- JSON is pretty-printed without a trailing newline and uses bDS2/Jason
-- byte-order key ordering. Project keys are blogLanguages,
-- blogmarkCategory, defaultAuthor, description, imageImportConcurrency,
-- mainLanguage, maxPostsPerPage, name, picoTheme, publicUrl,
-- semanticSimilarityEnabled (nil keys absent). Publishing keys are sshHost,
-- sshMode, sshRemotePath, sshUser (nil keys absent; sshMode defaults to scp).
-- Category names and per-category keys are byte-order sorted; per-category
-- keys are listTemplateSlug, postTemplateSlug, renderInLists, showTitle,
-- title. categories.json uses case-sensitive byte-order sorting. tags.json
-- remains case-insensitively name-sorted; entry keys are color, name,
-- postTemplateSlug and blank optional values are absent.
}
config {

View File

@@ -297,6 +297,20 @@ invariant FrontmatterRoundtrip {
parse_frontmatter(read_file(post.file_path)) = frontmatter_fields(post)
}
invariant CanonicalFrontmatterSerialization {
-- Post field order is id, title, slug, excerpt, status, author, language,
-- doNotTranslate, templateSlug, createdAt, updatedAt, publishedAt, tags,
-- categories; nil/empty optional fields and false doNotTranslate are absent.
-- Translation field order is id, translationFor, language, title, excerpt,
-- status, createdAt, updatedAt, publishedAt.
-- Strings containing only Unicode letters/numbers, space, dot, underscore,
-- slash, or hyphen are bare. Other strings are double quoted with backslash,
-- quote, and newline escaping. Timestamp values are single-quoted ISO values.
-- Empty lists use a bare key line. Written bodies have all trailing newlines
-- removed and the complete file ends in exactly one newline; reads remove
-- that terminal body newline before comparing or storing content.
}
invariant DateBasedFileLayout {
for post in Posts where file_path != "":
post.file_path = format("posts/{yyyy}/{mm}/{slug}.md",

View File

@@ -199,7 +199,10 @@ invariant ScriptFileLayout {
for s in Scripts where file_path != "":
s.file_path = format("scripts/{slug}.lua", slug: s.slug)
}
-- Script files use standard --- YAML frontmatter
-- Script files use standard --- YAML frontmatter in field order id, projectId,
-- slug, title, kind, entrypoint, enabled, version, createdAt, updatedAt. Scalars
-- use the canonical post quoting rules, timestamps are single-quoted ISO values,
-- and the complete file ends in one newline.
rule CreateScript {
when: CreateScriptRequested(project, title, kind, content, entrypoint)

View File

@@ -56,6 +56,9 @@ invariant UniqueTemplateSlug {
invariant TemplateFrontmatter {
-- .liquid files use standard --- YAML frontmatter
-- Fields: id, slug, title, kind, enabled, version, createdAt, updatedAt
-- Field order also includes projectId after id when present. Scalars use
-- the canonical post frontmatter quoting rules, timestamps are single-quoted
-- ISO values, and the complete file ends in one newline.
for t in Templates where status = published:
parse_frontmatter(read_file(t.file_path)).slug = t.slug
}