chore: tending to specs to clear out problems
This commit is contained in:
@@ -44,6 +44,27 @@ entity ChatMessage {
|
|||||||
created_at: Timestamp
|
created_at: Timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface OneShotAiSurface {
|
||||||
|
facing _: AiOperator
|
||||||
|
|
||||||
|
provides:
|
||||||
|
AnalyzeTaxonomyRequested(post)
|
||||||
|
AnalyzeImageRequested(media)
|
||||||
|
AnalyzePostRequested(post)
|
||||||
|
DetectLanguageRequested(text)
|
||||||
|
TranslatePostRequested(post, target_language)
|
||||||
|
TranslateMediaRequested(media, target_language)
|
||||||
|
}
|
||||||
|
|
||||||
|
surface AiChatSurface {
|
||||||
|
facing _: ChatOperator
|
||||||
|
|
||||||
|
provides:
|
||||||
|
StartChatRequested(model)
|
||||||
|
SendChatMessageRequested(conversation, content)
|
||||||
|
RefreshModelCatalogRequested(endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
-- One-shot AI tasks (core scope, no streaming)
|
-- One-shot AI tasks (core scope, no streaming)
|
||||||
-- All use OpenAI Chat Completions wire format.
|
-- All use OpenAI Chat Completions wire format.
|
||||||
-- Endpoint routing: see AirplaneModeGating invariant below.
|
-- Endpoint routing: see AirplaneModeGating invariant below.
|
||||||
@@ -95,13 +116,26 @@ rule TranslateMedia {
|
|||||||
|
|
||||||
rule StartChat {
|
rule StartChat {
|
||||||
when: StartChatRequested(model)
|
when: StartChatRequested(model)
|
||||||
ensures: ChatConversation.created(model: model)
|
ensures: ChatConversation.created(
|
||||||
|
title: generated_chat_title(model),
|
||||||
|
model: model,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
rule SendChatMessage {
|
rule SendChatMessage {
|
||||||
when: SendChatMessageRequested(conversation, content)
|
when: SendChatMessageRequested(conversation, content)
|
||||||
requires: active_endpoint_configured
|
requires: active_endpoint_configured
|
||||||
ensures: ChatMessage.created(conversation: conversation, role: user, content: content)
|
ensures: ChatMessage.created(
|
||||||
|
conversation: conversation,
|
||||||
|
role: user,
|
||||||
|
content: content,
|
||||||
|
token_usage_input: null,
|
||||||
|
token_usage_output: null,
|
||||||
|
created_at: now
|
||||||
|
)
|
||||||
|
ensures: conversation.updated_at = now
|
||||||
ensures: AiStreamingResponse(conversation)
|
ensures: AiStreamingResponse(conversation)
|
||||||
-- AI SDK v6 streamText() with tool-call loop (max 10 rounds)
|
-- AI SDK v6 streamText() with tool-call loop (max 10 rounds)
|
||||||
-- Blog data tools for post/media querying during chat
|
-- Blog data tools for post/media querying during chat
|
||||||
@@ -135,6 +169,13 @@ invariant TwoEndpointModel {
|
|||||||
-- This replaces the TypeScript app's 4 named provider model.
|
-- This replaces the TypeScript app's 4 named provider model.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invariant AiSpecPartitioning {
|
||||||
|
-- This file covers two distinct but related AI contracts:
|
||||||
|
-- 1. Core one-shot operations (taxonomy, vision, translation, language detection)
|
||||||
|
-- 2. Extension chat/model-catalog behaviour
|
||||||
|
-- Both share the same endpoint routing and airplane-mode gating rules.
|
||||||
|
}
|
||||||
|
|
||||||
invariant SecureKeyStorage {
|
invariant SecureKeyStorage {
|
||||||
-- API keys are never stored in plain text
|
-- API keys are never stored in plain text
|
||||||
-- Always encrypted via OS keychain before DB storage
|
-- Always encrypted via OS keychain before DB storage
|
||||||
|
|||||||
@@ -25,9 +25,31 @@ entity GitRepository {
|
|||||||
has_lfs: Boolean
|
has_lfs: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface GitControlSurface {
|
||||||
|
facing _: GitOperator
|
||||||
|
|
||||||
|
provides:
|
||||||
|
InitializeRepoRequested(project)
|
||||||
|
GitStatusRequested(project)
|
||||||
|
GitDiffRequested(project)
|
||||||
|
GitHistoryRequested(project, branch)
|
||||||
|
GitFetchRequested(project)
|
||||||
|
GitPullRequested(project)
|
||||||
|
GitPushRequested(project)
|
||||||
|
GitCommitAllRequested(project, message)
|
||||||
|
GitReconcileRequested(project, old_commit, new_commit)
|
||||||
|
PruneLfsCacheRequested(project, retain_recent)
|
||||||
|
}
|
||||||
|
|
||||||
rule InitializeRepo {
|
rule InitializeRepo {
|
||||||
when: InitializeRepoRequested(project)
|
when: InitializeRepoRequested(project)
|
||||||
ensures: GitRepository.created(is_initialized: true, has_lfs: true)
|
ensures: GitRepository.created(
|
||||||
|
is_initialized: true,
|
||||||
|
remote_url: null,
|
||||||
|
provider: null,
|
||||||
|
current_branch: "master",
|
||||||
|
has_lfs: true
|
||||||
|
)
|
||||||
ensures: GitignoreCreated(project)
|
ensures: GitignoreCreated(project)
|
||||||
-- .gitignore manages: thumbnails, generated html, node_modules, etc.
|
-- .gitignore manages: thumbnails, generated html, node_modules, etc.
|
||||||
ensures: LfsTrackingConfigured(project)
|
ensures: LfsTrackingConfigured(project)
|
||||||
@@ -62,7 +84,7 @@ rule Fetch {
|
|||||||
rule Pull {
|
rule Pull {
|
||||||
when: GitPullRequested(project)
|
when: GitPullRequested(project)
|
||||||
ensures: LocalBranchUpdated(project)
|
ensures: LocalBranchUpdated(project)
|
||||||
ensures: ReconcileFromGit(project)
|
ensures: GitReconcileRequested(project, previous_head(project), current_head(project))
|
||||||
-- After pull, detect changed files and reconcile DB
|
-- After pull, detect changed files and reconcile DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
126
specs/mcp.allium
126
specs/mcp.allium
@@ -22,6 +22,11 @@ entity Proposal {
|
|||||||
data: String
|
data: String
|
||||||
created_at: Timestamp
|
created_at: Timestamp
|
||||||
expires_at: Timestamp
|
expires_at: Timestamp
|
||||||
|
draft_post: post/Post?
|
||||||
|
proposed_script: script/Script?
|
||||||
|
proposed_template: template/Template?
|
||||||
|
target_media: media/Media?
|
||||||
|
target_post: post/Post?
|
||||||
|
|
||||||
-- Derived
|
-- Derived
|
||||||
is_expired: expires_at <= now
|
is_expired: expires_at <= now
|
||||||
@@ -39,6 +44,25 @@ config {
|
|||||||
proposal_ttl_cli: Duration = 8.hours
|
proposal_ttl_cli: Duration = 8.hours
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface McpAutomationSurface {
|
||||||
|
facing _: McpClient
|
||||||
|
|
||||||
|
provides:
|
||||||
|
McpToolInvoked("check_term", term)
|
||||||
|
McpToolInvoked("search_posts", params)
|
||||||
|
McpToolInvoked("count_posts", params)
|
||||||
|
McpToolInvoked("read_post_by_slug", slug, language)
|
||||||
|
McpToolInvoked("draft_post", params)
|
||||||
|
McpToolInvoked("propose_script", params)
|
||||||
|
McpToolInvoked("propose_template", params)
|
||||||
|
McpToolInvoked("propose_media_metadata", params)
|
||||||
|
McpToolInvoked("propose_post_metadata", params)
|
||||||
|
AcceptProposalRequested(proposal)
|
||||||
|
DiscardProposalRequested(proposal)
|
||||||
|
InstallAgentConfigRequested(agent_kind)
|
||||||
|
UninstallAgentConfigRequested(agent_kind)
|
||||||
|
}
|
||||||
|
|
||||||
invariant LocalhostOnlyHttp {
|
invariant LocalhostOnlyHttp {
|
||||||
-- HTTP transport binds to 127.0.0.1 only
|
-- HTTP transport binds to 127.0.0.1 only
|
||||||
-- Origin validation: localhost only
|
-- Origin validation: localhost only
|
||||||
@@ -161,7 +185,20 @@ rule DraftPost {
|
|||||||
content: params.content,
|
content: params.content,
|
||||||
status: draft
|
status: draft
|
||||||
)
|
)
|
||||||
Proposal.created(kind: draft_post, entity_id: new_post.id, status: pending)
|
let proposal = Proposal.created(
|
||||||
|
kind: draft_post,
|
||||||
|
entity_id: new_post.id,
|
||||||
|
data: "",
|
||||||
|
created_at: now,
|
||||||
|
expires_at: now + config.proposal_ttl_app,
|
||||||
|
draft_post: new_post,
|
||||||
|
proposed_script: null,
|
||||||
|
proposed_template: null,
|
||||||
|
target_media: null,
|
||||||
|
target_post: null,
|
||||||
|
status: pending
|
||||||
|
)
|
||||||
|
proposal.status = pending
|
||||||
}
|
}
|
||||||
|
|
||||||
rule ProposeScript {
|
rule ProposeScript {
|
||||||
@@ -174,7 +211,20 @@ rule ProposeScript {
|
|||||||
content: params.content,
|
content: params.content,
|
||||||
status: draft
|
status: draft
|
||||||
)
|
)
|
||||||
Proposal.created(kind: propose_script, entity_id: new_script.id, status: pending)
|
let proposal = Proposal.created(
|
||||||
|
kind: propose_script,
|
||||||
|
entity_id: new_script.id,
|
||||||
|
data: "",
|
||||||
|
created_at: now,
|
||||||
|
expires_at: now + config.proposal_ttl_app,
|
||||||
|
draft_post: null,
|
||||||
|
proposed_script: new_script,
|
||||||
|
proposed_template: null,
|
||||||
|
target_media: null,
|
||||||
|
target_post: null,
|
||||||
|
status: pending
|
||||||
|
)
|
||||||
|
proposal.status = pending
|
||||||
}
|
}
|
||||||
|
|
||||||
rule ProposeTemplate {
|
rule ProposeTemplate {
|
||||||
@@ -187,17 +237,58 @@ rule ProposeTemplate {
|
|||||||
content: params.content,
|
content: params.content,
|
||||||
status: draft
|
status: draft
|
||||||
)
|
)
|
||||||
Proposal.created(kind: propose_template, entity_id: new_template.id, status: pending)
|
let proposal = Proposal.created(
|
||||||
|
kind: propose_template,
|
||||||
|
entity_id: new_template.id,
|
||||||
|
data: "",
|
||||||
|
created_at: now,
|
||||||
|
expires_at: now + config.proposal_ttl_app,
|
||||||
|
draft_post: null,
|
||||||
|
proposed_script: null,
|
||||||
|
proposed_template: new_template,
|
||||||
|
target_media: null,
|
||||||
|
target_post: null,
|
||||||
|
status: pending
|
||||||
|
)
|
||||||
|
proposal.status = pending
|
||||||
}
|
}
|
||||||
|
|
||||||
rule ProposeMediaMetadata {
|
rule ProposeMediaMetadata {
|
||||||
when: McpToolInvoked("propose_media_metadata", params)
|
when: McpToolInvoked("propose_media_metadata", params)
|
||||||
ensures: Proposal.created(kind: propose_media_metadata, entity_id: params.media_id, data: params, status: pending)
|
ensures:
|
||||||
|
let proposal = Proposal.created(
|
||||||
|
kind: propose_media_metadata,
|
||||||
|
entity_id: params.media_id,
|
||||||
|
data: serialize(params),
|
||||||
|
created_at: now,
|
||||||
|
expires_at: now + config.proposal_ttl_app,
|
||||||
|
draft_post: null,
|
||||||
|
proposed_script: null,
|
||||||
|
proposed_template: null,
|
||||||
|
target_media: params.media,
|
||||||
|
target_post: null,
|
||||||
|
status: pending
|
||||||
|
)
|
||||||
|
proposal.status = pending
|
||||||
}
|
}
|
||||||
|
|
||||||
rule ProposePostMetadata {
|
rule ProposePostMetadata {
|
||||||
when: McpToolInvoked("propose_post_metadata", params)
|
when: McpToolInvoked("propose_post_metadata", params)
|
||||||
ensures: Proposal.created(kind: propose_post_metadata, entity_id: params.post_id, data: params, status: pending)
|
ensures:
|
||||||
|
let proposal = Proposal.created(
|
||||||
|
kind: propose_post_metadata,
|
||||||
|
entity_id: params.post_id,
|
||||||
|
data: serialize(params),
|
||||||
|
created_at: now,
|
||||||
|
expires_at: now + config.proposal_ttl_app,
|
||||||
|
draft_post: null,
|
||||||
|
proposed_script: null,
|
||||||
|
proposed_template: null,
|
||||||
|
target_media: null,
|
||||||
|
target_post: params.post,
|
||||||
|
status: pending
|
||||||
|
)
|
||||||
|
proposal.status = pending
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Proposal lifecycle
|
-- Proposal lifecycle
|
||||||
@@ -207,15 +298,16 @@ rule AcceptProposal {
|
|||||||
requires: not proposal.is_expired
|
requires: not proposal.is_expired
|
||||||
ensures:
|
ensures:
|
||||||
if proposal.kind = draft_post:
|
if proposal.kind = draft_post:
|
||||||
post/PublishPostRequested(proposal.entity_id)
|
post/PublishPostRequested(proposal.draft_post)
|
||||||
if proposal.kind = propose_script:
|
if proposal.kind = propose_script:
|
||||||
script/PublishScriptRequested(proposal.entity_id)
|
script/PublishScriptRequested(proposal.proposed_script)
|
||||||
if proposal.kind = propose_template:
|
if proposal.kind = propose_template:
|
||||||
template/PublishTemplateRequested(proposal.entity_id)
|
template/PublishTemplateRequested(proposal.proposed_template)
|
||||||
if proposal.kind = propose_media_metadata:
|
if proposal.kind = propose_media_metadata:
|
||||||
media/UpdateMediaRequested(proposal.entity_id, proposal.data)
|
media/UpdateMediaRequested(proposal.target_media, deserialize_media_changes(proposal.data))
|
||||||
if proposal.kind = propose_post_metadata:
|
if proposal.kind = propose_post_metadata:
|
||||||
post/UpdatePostRequested(proposal.entity_id, proposal.data)
|
post/UpdatePostRequested(proposal.target_post, deserialize_post_changes(proposal.data))
|
||||||
|
proposal.status = accepted
|
||||||
not exists proposal
|
not exists proposal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,17 +315,19 @@ rule DiscardProposal {
|
|||||||
when: DiscardProposalRequested(proposal)
|
when: DiscardProposalRequested(proposal)
|
||||||
ensures:
|
ensures:
|
||||||
if proposal.kind = draft_post:
|
if proposal.kind = draft_post:
|
||||||
post/DeletePostRequested(proposal.entity_id)
|
post/DeletePostRequested(proposal.draft_post)
|
||||||
if proposal.kind = propose_script:
|
if proposal.kind = propose_script:
|
||||||
script/DeleteScriptRequested(proposal.entity_id)
|
script/DeleteScriptRequested(proposal.proposed_script)
|
||||||
if proposal.kind = propose_template:
|
if proposal.kind = propose_template:
|
||||||
template/DeleteTemplateRequested(proposal.entity_id)
|
template/DeleteTemplateRequested(proposal.proposed_template)
|
||||||
|
proposal.status = discarded
|
||||||
not exists proposal
|
not exists proposal
|
||||||
}
|
}
|
||||||
|
|
||||||
rule ExpireProposal {
|
rule ExpireProposal {
|
||||||
when: proposal: Proposal.is_expired becomes true
|
when: proposal: Proposal.is_expired becomes true
|
||||||
-- On expiry: clean up draft DB rows
|
-- On expiry: clean up draft DB rows
|
||||||
|
ensures: proposal.status = expired
|
||||||
ensures: DiscardProposalRequested(proposal)
|
ensures: DiscardProposalRequested(proposal)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,3 +349,9 @@ rule UninstallAgentConfig {
|
|||||||
when: UninstallAgentConfigRequested(agent_kind)
|
when: UninstallAgentConfigRequested(agent_kind)
|
||||||
ensures: AgentConfigRemoved(agent_kind)
|
ensures: AgentConfigRemoved(agent_kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invariant ProposalPayloadEncoding {
|
||||||
|
-- Proposal.data stores a serialized payload for metadata proposals.
|
||||||
|
-- draft_post / propose_script / propose_template proposals keep the
|
||||||
|
-- created entity reference directly on the proposal record.
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,38 +25,94 @@ value UploadTarget {
|
|||||||
remote_dir: String
|
remote_dir: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface PublishingControlSurface {
|
||||||
|
facing _: PublishOperator
|
||||||
|
|
||||||
|
provides:
|
||||||
|
UploadSiteRequested(project, credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
surface PublishingRuntimeSurface {
|
||||||
|
facing _: PublishRuntime
|
||||||
|
|
||||||
|
provides:
|
||||||
|
PublishJobStarted(project, job, credentials)
|
||||||
|
PublishTargetFailed(job, target, error)
|
||||||
|
}
|
||||||
|
|
||||||
rule UploadSite {
|
rule UploadSite {
|
||||||
when: UploadSiteRequested(project, credentials)
|
when: UploadSiteRequested(project, credentials)
|
||||||
-- Three upload targets run as parallel tasks
|
ensures:
|
||||||
ensures: UploadTargetStarted(html, "html/", credentials.ssh_remote_path, credentials)
|
let job = PublishJob.created(
|
||||||
ensures: UploadTargetStarted(thumbnails, "thumbnails/", credentials.ssh_remote_path + "/thumbnails", credentials)
|
ssh_host: credentials.ssh_host,
|
||||||
ensures: UploadTargetStarted(media, "media/", credentials.ssh_remote_path + "/media", credentials)
|
ssh_user: credentials.ssh_user,
|
||||||
|
ssh_remote_path: credentials.ssh_remote_path,
|
||||||
|
ssh_mode: credentials.ssh_mode,
|
||||||
|
status: pending
|
||||||
|
)
|
||||||
|
job.status = pending
|
||||||
|
PublishJobStarted(project, job, credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule StartPublishJob {
|
||||||
|
when: PublishJobStarted(project, job, credentials)
|
||||||
|
ensures: job.status = running
|
||||||
|
ensures: UploadTargetStarted(job, html, "html/", credentials.ssh_remote_path, credentials)
|
||||||
|
ensures: UploadTargetStarted(job, thumbnails, "thumbnails/", credentials.ssh_remote_path + "/thumbnails", credentials)
|
||||||
|
ensures: UploadTargetStarted(job, media, "media/", credentials.ssh_remote_path + "/media", credentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
rule UploadViaScp {
|
rule UploadViaScp {
|
||||||
when: UploadTargetCompleted(target, credentials)
|
when: UploadTargetStarted(job, target, local_dir, remote_dir, credentials)
|
||||||
requires: credentials.ssh_mode = scp
|
requires: credentials.ssh_mode = scp
|
||||||
-- mtime-based upload detection: skip unchanged files
|
-- mtime-based upload detection: skip unchanged files
|
||||||
-- Uses SSH agent (SSH_AUTH_SOCK) for authentication
|
-- Uses SSH agent (SSH_AUTH_SOCK) for authentication
|
||||||
ensures: ScpUploadCompleted(target)
|
ensures: ScpUploadCompleted(job, target)
|
||||||
|
ensures: UploadTargetCompleted(job, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
rule UploadViaRsync {
|
rule UploadViaRsync {
|
||||||
when: UploadTargetCompleted(target, credentials)
|
when: UploadTargetStarted(job, target, local_dir, remote_dir, credentials)
|
||||||
requires: credentials.ssh_mode = rsync
|
requires: credentials.ssh_mode = rsync
|
||||||
-- rsync --update --compress --verbose
|
-- rsync --update --compress --verbose
|
||||||
-- Media uploads exclude .meta sidecar files
|
-- Media uploads exclude .meta sidecar files
|
||||||
ensures: RsyncUploadCompleted(target)
|
ensures: RsyncUploadCompleted(job, target)
|
||||||
|
ensures: UploadTargetCompleted(job, target)
|
||||||
|
|
||||||
@guidance
|
@guidance
|
||||||
-- rsync exclude filters for .meta files on media target
|
-- rsync exclude filters for .meta files on media target
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rule CompletePublishJob {
|
||||||
|
when: PublishTargetsCompleted(job)
|
||||||
|
requires: job.status = running
|
||||||
|
ensures: job.status = completed
|
||||||
|
}
|
||||||
|
|
||||||
|
rule FailPublishJob {
|
||||||
|
when: PublishTargetFailed(job, target, error)
|
||||||
|
requires: job.status = running
|
||||||
|
ensures: job.status = failed
|
||||||
|
}
|
||||||
|
|
||||||
|
rule TrackUploadCompletion {
|
||||||
|
when: UploadTargetCompleted(job, target)
|
||||||
|
requires: all_upload_targets_completed(job)
|
||||||
|
ensures: PublishTargetsCompleted(job)
|
||||||
|
}
|
||||||
|
|
||||||
invariant MediaSidecarsExcludedFromUpload {
|
invariant MediaSidecarsExcludedFromUpload {
|
||||||
-- .meta sidecar files are never uploaded to the remote server
|
-- .meta sidecar files are never uploaded to the remote server
|
||||||
-- They are project metadata, not public content
|
-- They are project metadata, not public content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invariant PublishJobLifecycle {
|
||||||
|
-- UploadSiteRequested creates one PublishJob in pending state.
|
||||||
|
-- PublishJobStarted moves the job to running before any target starts.
|
||||||
|
-- A job reaches completed only after PublishTargetsCompleted(job).
|
||||||
|
-- Any PublishTargetFailed(job, target, error) transitions the job to failed.
|
||||||
|
}
|
||||||
|
|
||||||
invariant SshAgentAuth {
|
invariant SshAgentAuth {
|
||||||
-- Publishing uses SSH_AUTH_SOCK for key-based authentication
|
-- Publishing uses SSH_AUTH_SOCK for key-based authentication
|
||||||
-- No password prompts, no interactive auth
|
-- No password prompts, no interactive auth
|
||||||
|
|||||||
@@ -26,6 +26,20 @@ entity Script {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface ScriptManagementSurface {
|
||||||
|
facing _: ScriptOperator
|
||||||
|
|
||||||
|
provides:
|
||||||
|
CreateScriptRequested(title, kind, content, entrypoint)
|
||||||
|
CreateAndPublishScriptRequested(title, kind, content, entrypoint)
|
||||||
|
PublishScriptRequested(script)
|
||||||
|
DeleteScriptRequested(script)
|
||||||
|
RunUtilityRequested(script)
|
||||||
|
MacroExpansionRequested(script, template_context)
|
||||||
|
BlogmarkReceived(data)
|
||||||
|
RebuildScriptsFromFilesRequested(project)
|
||||||
|
}
|
||||||
|
|
||||||
invariant UniqueScriptSlug {
|
invariant UniqueScriptSlug {
|
||||||
for a in Scripts:
|
for a in Scripts:
|
||||||
for b in Scripts:
|
for b in Scripts:
|
||||||
@@ -61,7 +75,8 @@ rule CreateAndPublishScript {
|
|||||||
when: CreateAndPublishScriptRequested(title, kind, content, entrypoint)
|
when: CreateAndPublishScriptRequested(title, kind, content, entrypoint)
|
||||||
let slug = slugify(title)
|
let slug = slugify(title)
|
||||||
requires: ValidateScript(content) = valid
|
requires: ValidateScript(content) = valid
|
||||||
ensures: Script.created(
|
ensures:
|
||||||
|
let new_script = Script.created(
|
||||||
slug: slug,
|
slug: slug,
|
||||||
title: title,
|
title: title,
|
||||||
kind: kind,
|
kind: kind,
|
||||||
@@ -72,7 +87,7 @@ rule CreateAndPublishScript {
|
|||||||
version: 1,
|
version: 1,
|
||||||
file_path: format("scripts/{slug}.lua", slug: slug)
|
file_path: format("scripts/{slug}.lua", slug: slug)
|
||||||
)
|
)
|
||||||
ensures: ScriptFileWritten(script)
|
ScriptFileWritten(new_script)
|
||||||
}
|
}
|
||||||
|
|
||||||
rule PublishScript {
|
rule PublishScript {
|
||||||
|
|||||||
@@ -27,6 +27,18 @@ entity Template {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface TemplateManagementSurface {
|
||||||
|
facing operator: TemplateOperator
|
||||||
|
|
||||||
|
provides:
|
||||||
|
CreateTemplateRequested(title, kind, content)
|
||||||
|
CreateAndPublishTemplateRequested(title, kind, content)
|
||||||
|
UpdateTemplateRequested(template, changes)
|
||||||
|
PublishTemplateRequested(template)
|
||||||
|
DeleteTemplateRequested(template)
|
||||||
|
RebuildTemplatesFromFilesRequested(project)
|
||||||
|
}
|
||||||
|
|
||||||
invariant UniqueTemplateSlug {
|
invariant UniqueTemplateSlug {
|
||||||
for a in Templates:
|
for a in Templates:
|
||||||
for b in Templates:
|
for b in Templates:
|
||||||
@@ -67,7 +79,8 @@ rule CreateAndPublishTemplate {
|
|||||||
when: CreateAndPublishTemplateRequested(title, kind, content)
|
when: CreateAndPublishTemplateRequested(title, kind, content)
|
||||||
let slug = slugify(title)
|
let slug = slugify(title)
|
||||||
requires: ValidateLiquid(content) = valid
|
requires: ValidateLiquid(content) = valid
|
||||||
ensures: Template.created(
|
ensures:
|
||||||
|
let new_template = Template.created(
|
||||||
slug: slug,
|
slug: slug,
|
||||||
title: title,
|
title: title,
|
||||||
kind: kind,
|
kind: kind,
|
||||||
@@ -77,7 +90,7 @@ rule CreateAndPublishTemplate {
|
|||||||
version: 1,
|
version: 1,
|
||||||
file_path: format("templates/{slug}.liquid", slug: slug)
|
file_path: format("templates/{slug}.liquid", slug: slug)
|
||||||
)
|
)
|
||||||
ensures: TemplateFileWritten(template)
|
TemplateFileWritten(new_template)
|
||||||
}
|
}
|
||||||
|
|
||||||
rule UpdateTemplate {
|
rule UpdateTemplate {
|
||||||
|
|||||||
Reference in New Issue
Block a user