agent forgot to commit the first allium spec
This commit is contained in:
parent
0a47d9d962
commit
806d95db6e
348
specs/scheduled-publishing.allium
Normal file
348
specs/scheduled-publishing.allium
Normal file
@ -0,0 +1,348 @@
|
||||
-- allium: 1
|
||||
-- scheduled-publishing.allium
|
||||
|
||||
-- Scope: Scheduled blog post publishing and authenticated author dashboard
|
||||
-- Includes: Post visibility rules, date-based scheduling, authentication,
|
||||
-- registration gating, author dashboard
|
||||
-- Excludes:
|
||||
-- - Post editing / CMS (posts are markdown files in git)
|
||||
-- - Role-based access (single role: authenticated user)
|
||||
-- - Multi-tenancy
|
||||
-- - Post creation workflow
|
||||
-- Constraints: Posts are compiled from markdown at build time (NimblePublisher).
|
||||
-- All filtering is runtime — no database for post content.
|
||||
|
||||
------------------------------------------------------------
|
||||
-- External Entities
|
||||
------------------------------------------------------------
|
||||
|
||||
-- Posts are compiled from markdown at build time, not managed by this spec
|
||||
external entity Post {
|
||||
id: String
|
||||
title: String
|
||||
author: String
|
||||
body: String
|
||||
description: String
|
||||
date: Date
|
||||
tags: Set<String>
|
||||
blog: Blog
|
||||
published: Boolean
|
||||
}
|
||||
|
||||
external entity Blog {
|
||||
blog_id: String
|
||||
title: String
|
||||
description: String
|
||||
base_path: String
|
||||
|
||||
posts: Post with blog = this
|
||||
}
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Enumerations
|
||||
------------------------------------------------------------
|
||||
|
||||
enum PostVisibility { live | scheduled | draft }
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Entities and Variants
|
||||
------------------------------------------------------------
|
||||
|
||||
entity User {
|
||||
email: String
|
||||
password_hash: String
|
||||
active_sessions: Session with user = this
|
||||
|
||||
-- Derived
|
||||
is_authenticated: active_sessions.count > 0
|
||||
}
|
||||
|
||||
entity Session {
|
||||
user: User
|
||||
token: String
|
||||
}
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Value Types
|
||||
------------------------------------------------------------
|
||||
|
||||
value PostWithVisibility {
|
||||
post: Post
|
||||
visibility: PostVisibility
|
||||
|
||||
-- Derived
|
||||
days_until_live: if visibility = scheduled: post.date - today else: null
|
||||
}
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Config
|
||||
------------------------------------------------------------
|
||||
|
||||
config {
|
||||
allowed_registration_email: String?
|
||||
}
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Defaults
|
||||
------------------------------------------------------------
|
||||
|
||||
-- Dev-only seed user
|
||||
default User demo_user = {
|
||||
email: "demo@example.com",
|
||||
password_hash: hash("password123")
|
||||
}
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Rules
|
||||
------------------------------------------------------------
|
||||
|
||||
-- Post visibility
|
||||
|
||||
rule DeterminePostVisibility {
|
||||
when: post: Post.created
|
||||
ensures:
|
||||
if not post.published:
|
||||
post.visibility = draft
|
||||
else if post.date > today:
|
||||
post.visibility = scheduled
|
||||
else:
|
||||
post.visibility = live
|
||||
}
|
||||
|
||||
rule FilterPublicPosts {
|
||||
when: PublicPostsRequested(blog)
|
||||
ensures: PublicPostsResolved(
|
||||
posts: blog.posts where published and date <= today
|
||||
)
|
||||
}
|
||||
|
||||
rule FilterPublicPostsByTag {
|
||||
when: PublicPostsByTagRequested(blog, tag)
|
||||
let matching = blog.posts where published and date <= today
|
||||
ensures: PublicPostsResolved(
|
||||
posts: matching where tag in tags
|
||||
)
|
||||
}
|
||||
|
||||
rule ResolvePostForPublic {
|
||||
when: PostRequested(blog, slug)
|
||||
let post = blog.posts where id = slug
|
||||
requires: exists post
|
||||
ensures: PostResolved(post: post)
|
||||
|
||||
-- No date/published filter — direct URL access always works
|
||||
guidance:
|
||||
-- Direct URL access is intentionally unfiltered so authors
|
||||
-- can share preview links with reviewers before publish date
|
||||
}
|
||||
|
||||
rule GenerateFeed {
|
||||
when: FeedRequested(blog)
|
||||
ensures: FeedResolved(
|
||||
posts: blog.posts where published and date <= today
|
||||
)
|
||||
}
|
||||
|
||||
-- Authentication
|
||||
|
||||
rule UserRegisters {
|
||||
when: RegisterRequested(email, password)
|
||||
requires: config.allowed_registration_email != null
|
||||
requires: email = config.allowed_registration_email
|
||||
ensures: User.created(
|
||||
email: email,
|
||||
password_hash: hash(password)
|
||||
)
|
||||
}
|
||||
|
||||
rule RegistrationRejectedWhenDisabled {
|
||||
when: RegisterRequested(email, password)
|
||||
requires: config.allowed_registration_email = null
|
||||
ensures: RegistrationRejected(reason: "registration is invite only.")
|
||||
}
|
||||
|
||||
rule RegistrationRejectedWhenEmailNotAllowed {
|
||||
when: RegisterRequested(email, password)
|
||||
requires: config.allowed_registration_email != null
|
||||
requires: email != config.allowed_registration_email
|
||||
ensures: RegistrationRejected(reason: "registration is invite only.")
|
||||
}
|
||||
|
||||
rule UserLogsIn {
|
||||
when: LoginRequested(email, password)
|
||||
let user = User{email}
|
||||
requires: exists user
|
||||
requires: verify(password, user.password_hash)
|
||||
ensures: Session.created(user: user)
|
||||
}
|
||||
|
||||
rule UserLogsOut {
|
||||
when: LogoutRequested(session)
|
||||
ensures: not exists session
|
||||
}
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Actor Declarations
|
||||
------------------------------------------------------------
|
||||
|
||||
actor Author {
|
||||
identified_by: User where is_authenticated
|
||||
}
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Surfaces
|
||||
------------------------------------------------------------
|
||||
|
||||
surface PublicBlogIndex {
|
||||
facing visitor: User
|
||||
|
||||
context blog: Blog
|
||||
|
||||
let visible_posts = blog.posts where published and date <= today
|
||||
|
||||
exposes:
|
||||
blog.title
|
||||
blog.description
|
||||
for post in visible_posts:
|
||||
post.title
|
||||
post.date
|
||||
post.author
|
||||
post.description
|
||||
post.tags
|
||||
|
||||
provides:
|
||||
PublicPostsRequested(blog)
|
||||
|
||||
related:
|
||||
PublicPostDetail(post) when post in visible_posts
|
||||
PublicBlogByTag(blog)
|
||||
}
|
||||
|
||||
surface PublicPostDetail {
|
||||
facing visitor: User
|
||||
|
||||
context post: Post
|
||||
|
||||
exposes:
|
||||
post.title
|
||||
post.date
|
||||
post.author
|
||||
post.body
|
||||
post.description
|
||||
post.tags
|
||||
post.visibility
|
||||
|
||||
guidance:
|
||||
-- Accessible by direct URL regardless of date or published status.
|
||||
-- This allows authors to share preview links with reviewers.
|
||||
-- When viewer is authenticated and post is not live:
|
||||
-- draft → banner: "Draft — not published"
|
||||
-- scheduled → banner: "This post is scheduled for {date}"
|
||||
}
|
||||
|
||||
surface PublicBlogByTag {
|
||||
facing visitor: User
|
||||
|
||||
context blog: Blog
|
||||
|
||||
let visible_posts = blog.posts where published and date <= today
|
||||
|
||||
exposes:
|
||||
for tag in blog.all_tags:
|
||||
tag
|
||||
for post in visible_posts where tag in post.tags:
|
||||
post.title
|
||||
post.date
|
||||
|
||||
provides:
|
||||
PublicPostsByTagRequested(blog, tag)
|
||||
}
|
||||
|
||||
surface PublicFeed {
|
||||
facing visitor: User
|
||||
|
||||
context blog: Blog
|
||||
|
||||
exposes:
|
||||
blog.title
|
||||
blog.description
|
||||
for post in blog.posts where published and date <= today:
|
||||
post.title
|
||||
post.date
|
||||
post.author
|
||||
post.description
|
||||
post.body
|
||||
|
||||
provides:
|
||||
FeedRequested(blog)
|
||||
|
||||
guidance:
|
||||
-- Serves RSS and Atom feeds.
|
||||
-- Only includes posts that are published and not future-dated.
|
||||
-- Feeds reflect the filtered list at request time; no cache-busting needed.
|
||||
}
|
||||
|
||||
surface EditorDashboard {
|
||||
facing author: Author
|
||||
|
||||
context blog: Blog
|
||||
|
||||
let drafts = blog.posts where not published
|
||||
let scheduled = blog.posts where published and date > today
|
||||
|
||||
exposes:
|
||||
blog.title
|
||||
for draft in drafts:
|
||||
draft.title
|
||||
draft.date
|
||||
draft.author
|
||||
draft.blog
|
||||
draft.visibility
|
||||
for scheduled_post in scheduled:
|
||||
scheduled_post.title
|
||||
scheduled_post.date
|
||||
scheduled_post.author
|
||||
scheduled_post.blog
|
||||
scheduled_post.visibility
|
||||
scheduled_post.days_until_live
|
||||
|
||||
guidance:
|
||||
-- Dashboard at /editor/dashboard.
|
||||
-- Drafts and scheduled posts shown in separate tabs.
|
||||
-- Unified timeline across all blogs (not grouped by blog).
|
||||
-- Drafts sorted by date descending.
|
||||
-- Scheduled posts sorted by date ascending (soonest first).
|
||||
-- Scheduled posts show "X days until live" countdown.
|
||||
|
||||
related:
|
||||
PublicPostDetail(post) when post in drafts or post in scheduled
|
||||
}
|
||||
|
||||
surface Login {
|
||||
facing visitor: User
|
||||
|
||||
provides:
|
||||
LoginRequested(email, password)
|
||||
|
||||
guidance:
|
||||
-- Login page is not linked from public navigation.
|
||||
-- Authors access it directly by URL.
|
||||
}
|
||||
|
||||
surface Registration {
|
||||
facing visitor: User
|
||||
|
||||
provides:
|
||||
RegisterRequested(email, password)
|
||||
|
||||
guidance:
|
||||
-- Registration page is not linked from public navigation.
|
||||
-- Rejects with "registration is invite only." when email does not
|
||||
-- match ALLOWED_REGISTRATION_EMAIL or when the env var is unset.
|
||||
}
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Open Questions
|
||||
------------------------------------------------------------
|
||||
|
||||
-- No open questions remaining
|
||||
Loading…
x
Reference in New Issue
Block a user