diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 00:58:15 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 00:58:15 +0800 |
| commit | 7b29dbff7e43e87741476f47df17ad1582b16546 (patch) | |
| tree | 4c9d0832b4e4edd77e59e9cb0a88abf5e729a9f9 /src/SomhairlesDream.Shared/Artifact.fs | |
| parent | 47add3088d50f12c5f48591f26c53cdb59d8283b (diff) | |
| download | somhairles-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.fs | 218 |
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 } |
