chore: brought RuDS up to bDS2 alignment, as that is the new baseline we want to hit
This commit is contained in:
@@ -5,14 +5,20 @@
|
||||
|
||||
use "./project.allium" as project
|
||||
|
||||
enum PostStatus {
|
||||
draft
|
||||
published
|
||||
archived
|
||||
}
|
||||
|
||||
value Slug {
|
||||
value: String
|
||||
|
||||
-- Generated by: transliterate unicode to ASCII, lowercase,
|
||||
-- replace [^a-z0-9]+ with hyphens, strip leading/trailing hyphens
|
||||
-- Transliteration scope: only German (ä/ö/ü/ß/ÄÖÜ) and English letters used.
|
||||
-- Verify deunicode handles this set correctly against transliteration npm.
|
||||
-- Uniqueness: tries base, then {slug}-2 .. {slug}-999, then {slug}-{timestamp}
|
||||
-- Verify transliteration matches the established bDS behaviour for this set.
|
||||
-- Uniqueness: tries base, then {slug}-2, {slug}-3, … (unbounded numeric suffix)
|
||||
}
|
||||
|
||||
value PostFilePath {
|
||||
@@ -40,13 +46,51 @@ value Frontmatter {
|
||||
-- doNotTranslate (only when true), templateSlug, publishedAt
|
||||
}
|
||||
|
||||
surface PostControlSurface {
|
||||
facing _: PostOperator
|
||||
|
||||
provides:
|
||||
CreatePostRequested(project, title, content, tags, categories, author, language, template_slug)
|
||||
UpdatePostRequested(post, changes)
|
||||
PublishPostRequested(post)
|
||||
DeletePostRequested(post)
|
||||
ArchivePostRequested(post)
|
||||
DiscardPostChangesRequested(post)
|
||||
SyncPostFromFileRequested(post)
|
||||
ImportOrphanPostFileRequested(project, relative_path)
|
||||
}
|
||||
|
||||
surface PostFilePathSurface {
|
||||
context path: PostFilePath
|
||||
|
||||
exposes:
|
||||
path.base_dir
|
||||
path.year
|
||||
path.month
|
||||
path.slug
|
||||
}
|
||||
|
||||
surface PostCanonicalUrlSurface {
|
||||
context url: PostCanonicalUrl
|
||||
|
||||
exposes:
|
||||
url.year
|
||||
url.month
|
||||
url.day
|
||||
url.slug
|
||||
}
|
||||
|
||||
surface FrontmatterSurface {
|
||||
context _: Frontmatter
|
||||
}
|
||||
|
||||
entity Post {
|
||||
project: project/Project
|
||||
title: String
|
||||
slug: Slug
|
||||
excerpt: String?
|
||||
content: String?
|
||||
status: draft | published | archived
|
||||
status: PostStatus
|
||||
author: String?
|
||||
language: String?
|
||||
do_not_translate: Boolean
|
||||
@@ -59,6 +103,15 @@ entity Post {
|
||||
updated_at: Timestamp
|
||||
published_at: Timestamp?
|
||||
|
||||
-- Published snapshot: copy of title/content/tags/categories/excerpt as of
|
||||
-- the last publish. Used by changes_affect_published_content to decide when
|
||||
-- an edit reopens a published post to draft (see ReopenPublishedPost).
|
||||
published_title: String?
|
||||
published_content: String?
|
||||
published_tags: String?
|
||||
published_categories: String?
|
||||
published_excerpt: String?
|
||||
|
||||
-- Relationships
|
||||
translations: PostTranslation with canonical_post = this
|
||||
linked_media: PostMediaLink with post = this
|
||||
@@ -71,6 +124,10 @@ entity Post {
|
||||
-- Slug changes only allowed before first publish
|
||||
content_location: if status = published: file_path else: content
|
||||
-- Published: body in filesystem. Draft: body in DB field.
|
||||
editor_body: if content != null: content else: read_markdown_body(file_path)
|
||||
-- Resolver used by editors: prefer the in-DB draft content, else read
|
||||
-- the markdown body from the post's file. Empty string when neither
|
||||
-- exists. The same resolver applies to PostTranslation records.
|
||||
|
||||
transitions status {
|
||||
draft -> published
|
||||
@@ -104,21 +161,23 @@ rule CreatePost {
|
||||
when: CreatePostRequested(project, title, content, tags, categories, author, language, template_slug)
|
||||
let slug = Slug.generate(title ?? "untitled")
|
||||
let unique_slug = Slug.ensure_unique(slug, project)
|
||||
ensures: Post.created(
|
||||
project: project,
|
||||
title: title ?? "",
|
||||
slug: unique_slug,
|
||||
content: content,
|
||||
status: draft,
|
||||
author: author,
|
||||
language: language,
|
||||
tags: tags ?? {},
|
||||
categories: categories ?? {},
|
||||
template_slug: template_slug,
|
||||
do_not_translate: false,
|
||||
file_path: ""
|
||||
)
|
||||
ensures: SearchIndexUpdated(post)
|
||||
ensures:
|
||||
let new_post = Post.created(
|
||||
project: project,
|
||||
title: title ?? "",
|
||||
slug: unique_slug,
|
||||
content: content,
|
||||
status: draft,
|
||||
author: author,
|
||||
language: language,
|
||||
tags: tags ?? {},
|
||||
categories: categories ?? {},
|
||||
template_slug: template_slug,
|
||||
do_not_translate: false,
|
||||
file_path: ""
|
||||
)
|
||||
new_post.status = draft
|
||||
SearchIndexUpdated(new_post)
|
||||
}
|
||||
|
||||
rule UpdatePost {
|
||||
@@ -133,6 +192,13 @@ rule UpdatePost {
|
||||
-- status auto-transitions back to draft
|
||||
}
|
||||
|
||||
rule ReopenPublishedPost {
|
||||
when: UpdatePostRequested(post, changes)
|
||||
requires: post.status = published
|
||||
requires: changes_affect_published_content(changes)
|
||||
ensures: post.status = draft
|
||||
}
|
||||
|
||||
rule PublishPost {
|
||||
when: PublishPostRequested(post)
|
||||
requires: post.status = draft or post.status = archived
|
||||
@@ -164,9 +230,43 @@ rule DeletePost {
|
||||
|
||||
rule ArchivePost {
|
||||
when: ArchivePostRequested(post)
|
||||
requires: post.status = draft or post.status = published
|
||||
ensures: post.status = archived
|
||||
}
|
||||
|
||||
rule SyncPostFromFile {
|
||||
when: SyncPostFromFileRequested(post)
|
||||
requires: post.file_path != ""
|
||||
-- Single-post reimport: re-reads the post's own .md file and upserts the DB
|
||||
-- record from it (the filesystem is treated as truth for that one post).
|
||||
-- Re-syncs the post's link graph. Errors out if the file is missing.
|
||||
ensures: PostFieldsUpdated(post, parse_post_file(post.file_path))
|
||||
ensures: PostLinksUpdated(post)
|
||||
}
|
||||
|
||||
rule ImportOrphanPostFile {
|
||||
when: ImportOrphanPostFileRequested(project, relative_path)
|
||||
-- Imports a .md file that exists on disk but has no DB record (an orphan,
|
||||
-- e.g. surfaced by RunMetadataDiff). Rejects non-markdown / missing files.
|
||||
ensures:
|
||||
let new_post = Post.created(parse_post_file(relative_path))
|
||||
SearchIndexUpdated(new_post)
|
||||
}
|
||||
|
||||
rule DiscardPostChanges {
|
||||
when: DiscardPostChangesRequested(post)
|
||||
requires: post.file_path != ""
|
||||
-- Only posts with a published file on disk can be discarded;
|
||||
-- a never-published draft has no file version to restore.
|
||||
-- Re-reads the published .md file and upserts the DB record from it,
|
||||
-- discarding unsaved draft edits.
|
||||
ensures: post.content = null
|
||||
ensures: post.status = published
|
||||
ensures: PostLinksUpdated(post)
|
||||
ensures: SearchIndexUpdated(post)
|
||||
-- See engine_side_effects.allium DiscardPostChangesSideEffects
|
||||
}
|
||||
|
||||
-- File format axioms
|
||||
|
||||
invariant FrontmatterRoundtrip {
|
||||
@@ -185,5 +285,5 @@ invariant DateBasedFileLayout {
|
||||
}
|
||||
|
||||
-- Slug freeze: once published_at is set, the slug is permanently frozen.
|
||||
-- This matches TypeScript behaviour: is_slug_frozen = published_at != null
|
||||
-- This follows the established bDS rule: is_slug_frozen = published_at != null
|
||||
-- Even if the post reverts to draft, the slug cannot be changed.
|
||||
|
||||
Reference in New Issue
Block a user