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