122 lines
3.3 KiB
Plaintext
122 lines
3.3 KiB
Plaintext
-- allium: 1
|
|
-- bDS Tag System
|
|
-- Scope: core (Wave 1)
|
|
-- Distilled from: src/main/engine/TagEngine.ts, schema.ts
|
|
|
|
use "./project.allium" as project
|
|
use "./post.allium" as post
|
|
|
|
surface TagControlSurface {
|
|
facing _: TagOperator
|
|
|
|
provides:
|
|
CreateTagRequested(project, name, color)
|
|
UpdateTagRequested(tag, changes)
|
|
DeleteTagRequested(tag)
|
|
RenameTagRequested(tag, new_name)
|
|
MergeTagsRequested(sources, target)
|
|
SyncTagsFromPostsRequested(project)
|
|
}
|
|
|
|
entity Tag {
|
|
project: project/Project
|
|
name: String
|
|
color: String? -- hex color code
|
|
post_template_slug: String?
|
|
created_at: Timestamp
|
|
updated_at: Timestamp
|
|
|
|
-- Derived
|
|
posts: post/Post with this.name in tags
|
|
post_count: posts.count
|
|
}
|
|
|
|
surface TagSurface {
|
|
context tag: Tag
|
|
|
|
exposes:
|
|
tag.project
|
|
tag.name
|
|
tag.color when tag.color != null
|
|
tag.post_template_slug when tag.post_template_slug != null
|
|
tag.created_at
|
|
tag.updated_at
|
|
tag.posts.count
|
|
tag.post_count
|
|
}
|
|
|
|
invariant UniqueTagNamePerProject {
|
|
-- Case-insensitive uniqueness
|
|
for a in Tags:
|
|
for b in Tags:
|
|
(a != b and a.project = b.project)
|
|
implies lowercase(a.name) != lowercase(b.name)
|
|
}
|
|
|
|
invariant TagsPersistToFilesystem {
|
|
-- meta/tags.json is the portable format (no internal IDs)
|
|
-- Must stay in sync with DB tag table
|
|
parse_json(read_file("meta/tags.json")) = serialize_portable(Tags)
|
|
}
|
|
|
|
rule CreateTag {
|
|
when: CreateTagRequested(project, name, color)
|
|
let existing_tags = Tags where project = project
|
|
requires: not existing_tags.any(t => lowercase(t.name) = lowercase(name))
|
|
-- Case-insensitive duplicate check
|
|
ensures: Tag.created(
|
|
project: project,
|
|
name: name,
|
|
color: color
|
|
)
|
|
ensures: TagsFileWritten(project)
|
|
}
|
|
|
|
rule UpdateTag {
|
|
when: UpdateTagRequested(tag, changes)
|
|
ensures: TagFieldsUpdated(tag, changes)
|
|
ensures: tag.updated_at = now
|
|
ensures: TagsFileWritten(tag.project)
|
|
}
|
|
|
|
rule DeleteTag {
|
|
when: DeleteTagRequested(tag)
|
|
-- Runs as background task, removes tag from all posts
|
|
for p in tag.posts:
|
|
ensures: p.tags = p.tags - {tag.name}
|
|
ensures: not exists tag
|
|
ensures: TagsFileWritten(tag.project)
|
|
}
|
|
|
|
rule RenameTag {
|
|
when: RenameTagRequested(tag, new_name)
|
|
-- Runs as background task
|
|
let old_name = tag.name
|
|
for p in tag.posts:
|
|
ensures: p.tags = (p.tags - {old_name}) + {new_name}
|
|
ensures: tag.name = new_name
|
|
ensures: TagsFileWritten(tag.project)
|
|
}
|
|
|
|
rule MergeTags {
|
|
when: MergeTagsRequested(sources, target)
|
|
-- Runs as background task
|
|
-- Merges multiple source tags into a single target
|
|
requires: sources.count >= 1
|
|
for source in sources:
|
|
for p in source.posts:
|
|
ensures: p.tags = (p.tags - {source.name}) + {target.name}
|
|
ensures: not exists source
|
|
ensures: TagsFileWritten(target.project)
|
|
}
|
|
|
|
rule SyncTagsFromPosts {
|
|
when: SyncTagsFromPostsRequested(project)
|
|
-- Discovers tags used in posts that are not in the tags table
|
|
for post in project.posts:
|
|
for tag_name in post.tags:
|
|
if not exists Tag{project: project, name: tag_name}:
|
|
ensures: Tag.created(project: project, name: tag_name)
|
|
ensures: TagsFileWritten(project)
|
|
}
|