Files
bDS2/specs/tag.allium
2026-04-23 10:42:27 +02:00

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)
}