summaryrefslogtreecommitdiff
path: root/src/SomhairlesDream.Shared/Artifact.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 00:58:15 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 00:58:15 +0800
commit7b29dbff7e43e87741476f47df17ad1582b16546 (patch)
tree4c9d0832b4e4edd77e59e9cb0a88abf5e729a9f9 /src/SomhairlesDream.Shared/Artifact.fs
parent47add3088d50f12c5f48591f26c53cdb59d8283b (diff)
downloadsomhairles-dream-fsharp-7b29dbff7e43e87741476f47df17ad1582b16546.tar.gz
feat(pipeline): add runnable artifact pipeline increment
[Change Nature] - This commit adds the first runnable increment of the F# artifact pipeline: CLI, server, frontend bundle, stage fixture, and tests. It is feature work, not a bug fix. [New Capability] - CLI `run` executes the three-step Blender fixture through the process bridge and writes GLBs, step logs, events, and a manifest; `verify` validates a manifest without Blender. - Server exposes run start, run-scoped SSE, artifact manifest/file routes, and serves the Fable frontend from its build output. - Frontend starts runs, follows run-scoped SSE progress, and links the artifact manifest on completion. [Implementation] - Shared layer holds domain, run state, replay, and artifact contracts; Modeling holds the pipeline, 120s bridge timeout, and result-file protocol; stage/blender/fixture.py owns geometry in Python (bpy owns geometry, F# owns orchestration). - Fable bundle is regenerated into public/ and copied by the server project; solution includes all projects with warnings-as-errors. - README documents build, Fable, server, interpreter/env, fixture limitations, and current verification status. [Impact] - Verified: 40/40 solution tests pass; Release build 0 warnings/errors; real CLI run with bpy 5.0.1 produced 3 GLBs, 3 logs, and a verify-clean manifest; isolated-port server smoke flow succeeded. - Not yet verified: browser end-to-end behavior and production fidelity. Artifacts and caches are gitignored.
Diffstat (limited to 'src/SomhairlesDream.Shared/Artifact.fs')
-rw-r--r--src/SomhairlesDream.Shared/Artifact.fs218
1 files changed, 218 insertions, 0 deletions
diff --git a/src/SomhairlesDream.Shared/Artifact.fs b/src/SomhairlesDream.Shared/Artifact.fs
new file mode 100644
index 0000000..d1cb4f0
--- /dev/null
+++ b/src/SomhairlesDream.Shared/Artifact.fs
@@ -0,0 +1,218 @@
+namespace SomhairlesDream.Shared
+
+open System
+#if !FABLE_COMPILER
+open System.Text.Json
+open System.Text.Json.Nodes
+#endif
+
+[<CLIMutable>]
+type RunIds =
+ { ProjectId: string
+ RunId: string
+ BaseId: string
+ TargetId: string }
+
+module Ids =
+ open System.Text.RegularExpressions
+
+ let private resourcePattern = Regex("^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", RegexOptions.CultureInvariant)
+ let private objectPattern = Regex("^[a-z0-9]+(?:[._-][a-z0-9]+)*$", RegexOptions.CultureInvariant)
+
+ let validateResourceId value =
+ if isNull value || not (resourcePattern.IsMatch(value)) then
+ Error "resource id must contain only lower-case letters, digits, and hyphens"
+ else
+ Ok ()
+
+ let validateObjectId value =
+ if isNull value || not (objectPattern.IsMatch(value)) then
+ Error "object id must use the dotted lower-case namespace"
+ else
+ Ok ()
+
+[<CLIMutable>]
+type RunStart =
+ { Ids: RunIds
+ At: DateTimeOffset
+ TotalSteps: int }
+
+[<CLIMutable>]
+type RunHeartbeat =
+ { Ids: RunIds
+ At: DateTimeOffset
+ Message: string }
+
+[<CLIMutable>]
+type RunCheckpoint =
+ { Ids: RunIds
+ At: DateTimeOffset
+ StepId: string
+ StepIndex: int
+ TotalSteps: int
+ Message: string
+ ArtifactPath: string }
+
+[<CLIMutable>]
+type RunComplete =
+ { Ids: RunIds
+ At: DateTimeOffset
+ ManifestPath: string }
+
+[<CLIMutable>]
+type RunFail =
+ { Ids: RunIds
+ At: DateTimeOffset
+ StepId: string option
+ Message: string }
+
+type RunEvent =
+ | Start of RunStart
+ | Heartbeat of RunHeartbeat
+ | Checkpoint of RunCheckpoint
+ | Complete of RunComplete
+ | Fail of RunFail
+
+[<CLIMutable>]
+type RunEventWire =
+ { Kind: string
+ At: DateTimeOffset
+ ProjectId: string
+ RunId: string
+ BaseId: string
+ TargetId: string
+ TotalSteps: int option
+ StepId: string option
+ StepIndex: int option
+ Message: string option
+ ArtifactPath: string option
+ ManifestPath: string option }
+
+module RunEvent =
+ let private ids (value: RunIds) =
+ value.ProjectId, value.RunId, value.BaseId, value.TargetId
+
+ let toWire event =
+ let kind, eventIds, at, totalSteps, stepId, stepIndex, message, artifactPath, manifestPath =
+ match event with
+ | Start value ->
+ "start", value.Ids, value.At, Some value.TotalSteps, None, None, None, None, None
+ | Heartbeat value ->
+ "heartbeat", value.Ids, value.At, None, None, None, Some value.Message, None, None
+ | Checkpoint value ->
+ "checkpoint",
+ value.Ids,
+ value.At,
+ Some value.TotalSteps,
+ Some value.StepId,
+ Some value.StepIndex,
+ Some value.Message,
+ Some value.ArtifactPath,
+ None
+ | Complete value ->
+ "complete", value.Ids, value.At, None, None, None, None, None, Some value.ManifestPath
+ | Fail value ->
+ "fail", value.Ids, value.At, None, value.StepId, None, Some value.Message, None, None
+
+ let projectId, runId, baseId, targetId = ids eventIds
+
+ { Kind = kind
+ At = at
+ ProjectId = projectId
+ RunId = runId
+ BaseId = baseId
+ TargetId = targetId
+ TotalSteps = totalSteps
+ StepId = stepId
+ StepIndex = stepIndex
+ Message = message
+ ArtifactPath = artifactPath
+ ManifestPath = manifestPath }
+
+#if !FABLE_COMPILER
+ let private addOptional (json: JsonObject) (name: string) value create =
+ match value with
+ | Some item -> json[name] <- create item
+ | None -> ()
+
+ let private jsonOptions () =
+ let options = JsonSerializerOptions()
+ options.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase
+ options.WriteIndented <- false
+ options
+
+ let toJson event =
+ let wire = toWire event
+ let json = JsonObject()
+ json["kind"] <- JsonValue.Create(wire.Kind)
+ json["at"] <- JsonValue.Create(wire.At)
+ json["projectId"] <- JsonValue.Create(wire.ProjectId)
+ json["runId"] <- JsonValue.Create(wire.RunId)
+ json["baseId"] <- JsonValue.Create(wire.BaseId)
+ json["targetId"] <- JsonValue.Create(wire.TargetId)
+ addOptional json "totalSteps" wire.TotalSteps (fun value -> JsonValue.Create(value))
+ addOptional json "stepId" wire.StepId (fun value -> JsonValue.Create(value))
+ addOptional json "stepIndex" wire.StepIndex (fun value -> JsonValue.Create(value))
+ addOptional json "message" wire.Message (fun value -> JsonValue.Create(value))
+ addOptional json "artifactPath" wire.ArtifactPath (fun value -> JsonValue.Create(value))
+ addOptional json "manifestPath" wire.ManifestPath (fun value -> JsonValue.Create(value))
+ json.ToJsonString(jsonOptions())
+#endif
+
+[<CLIMutable>]
+type ArtifactChange =
+ { ObjectId: string
+ Kind: string
+ Summary: string }
+
+[<CLIMutable>]
+type RenderArtifact =
+ { ArtifactPath: string
+ Sha256: string
+ Bytes: int64
+ ExportedAt: DateTimeOffset }
+
+[<CLIMutable>]
+type StepArtifact =
+ { StepId: string
+ StepIndex: int
+ ArtifactPath: string
+ LogPath: string
+ Sha256: string
+ Bytes: int64
+ ExportedAt: DateTimeOffset
+ ObjectIds: string array
+ VerifiedObjectIds: string array
+ Changes: ArtifactChange array
+ Render: RenderArtifact option }
+
+[<CLIMutable>]
+type ArtifactManifest =
+ { SchemaVersion: int
+ ProjectId: string
+ RunId: string
+ BaseId: string
+ TargetId: string
+ Status: string
+ StartedAt: DateTimeOffset
+ CompletedAt: DateTimeOffset
+ BlenderVersion: string
+ Steps: StepArtifact array }
+
+[<CLIMutable>]
+type RunSnapshot =
+ { ProjectId: string
+ RunId: string
+ BaseId: string
+ TargetId: string
+ Status: string
+ CurrentStepId: string option
+ CurrentStepIndex: int
+ TotalSteps: int
+ LastHeartbeatAt: DateTimeOffset option
+ UpdatedAt: DateTimeOffset
+ Message: string
+ ManifestPath: string option
+ ManifestUrl: string option
+ Error: string option
+ IsStale: bool }