summaryrefslogtreecommitdiff
path: root/README.md
blob: 907fc50183366df99bbfaa27e0c15636c4af7c84 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# somhairles-dream-fsharp

F# implementation of the heritage boat modeling pipeline: a Blender-driven
artifact pipeline (CLI), an ASP.NET server that runs and observes artifact
runs, and a Fable/Three.js frontend that follows run progress live.

Architecture boundary: F# owns the frontend, backend, and orchestration
(pipeline, artifacts, verification, serving). Python (`bpy`) owns geometry
generation inside `stage/blender/fixture.py`. There is no F# geometry DSL.

## Layout

- `src/SomhairlesDream.Shared` — domain, run state, replay, artifact contracts
- `src/SomhairlesDream.Modeling` — pipeline, Blender process bridge, verifier
- `src/SomhairlesDream.Cli` — `run` and `verify` commands
- `src/SomhairlesDream.Server` — run API, SSE, static frontend host
- `src/SomhairlesDream.Frontend` — Fable browser app compiled to `public/`
- `stage/blender/fixture.py` — deterministic bpy fixture (3 steps, 7 objects)
- `public/` — static frontend served by the server (index, styles, JS bundle)
- `tests/` — xUnit test projects per layer

## Build and test

Requires .NET SDK 8 (pinned by `global.json`). Warnings are errors.

```sh
dotnet build SomhairlesDream.sln
dotnet test SomhairlesDream.sln
```

## Frontend (Fable)

`src/SomhairlesDream.Frontend` compiles to `public/`. Regenerate after edits:

```sh
dotnet fable src/SomhairlesDream.Frontend/SomhairlesDream.Frontend.fsproj \
  --outDir public --noRestore
```

The server project copies `public/**` into its build output
(`CopyToOutputDirectory=PreserveNewest`), so rerun `dotnet build` on the server
after regenerating the bundle.

Three.js r160 and GLTFLoader are vendored under `public/vendor/` (import map
plus a `three-setup.js` shim exposing `window.THREE`/`window.GLTFLoader`).
They are MIT-licensed third-party dependencies, not project assets.

## Browser acceptance driver

`tools/acceptance/e2e_acceptance.py` is the checked-in end-to-end gate. It
starts the server on an isolated port with its own artifact root, launches
headless chromium, and drives a full run through the page form twice
(desktop 1440x1000 and narrow 390x844 viewports, `?freeze=1`), plus a
failure-path run. It fails unless every check passes; exit code 0 means
green. Checks include:

- idle page (`待机`, empty-state text), SSE connection, live per-stage
  viewport ingestion (`运行中` + `steps/01-foundation.glb`, then `已完成` +
  `steps/03-cabin.glb`), manifest link exposure
- geometry framing asserted from the live scene (`window.SOMHAIRLES_VIEWER`
  + `THREE.Box3` projected to screen space): model fully inside the canvas
  with margins >= 2% of the smaller canvas dimension, pixel coverage within
  40%-95%, center offset <= 10%, on both viewports; stage-1 vs stage-3
  bounding boxes must differ by >= 5%
- frozen-viewport determinism: two consecutive canvas screenshots are
  byte-identical (sha256)
- served GLB sha256 equality with pinned reference hashes
  (`01-foundation`, `03-cabin`) and `model/gltf-binary` content type
- failure run (pre-existing run directory) surfaces `失败` and the pipeline
  error in `run-message`; zero browser console/page errors overall

Playwright is self-provisioned into the gitignored
`tools/acceptance/deps/` directory (`pip --target --break-system-packages`,
pinned to an already-installed version when detectable) so the driver never
installs into system site-packages; the chromium browser binary is reused
from the user cache when present.

Full verification from a clean checkout:

```sh
dotnet tool restore
# Fable must run BEFORE the solution build: the server serves the frontend
# from public/ (copied into wwwroot at build time), and a fresh checkout has
# no fable_modules/App.js yet. Also run the frontend project restore first,
# and use --noCache: fable's project-options cache (keyed by fsproj content)
# is shared across checkouts of the same project and can yield spurious
# "Fable namespace not defined" errors on a fresh clone.
dotnet restore src/SomhairlesDream.Frontend/SomhairlesDream.Frontend.fsproj
dotnet fable src/SomhairlesDream.Frontend/SomhairlesDream.Frontend.fsproj \
  --outDir public --noRestore --noCache
dotnet build SomhairlesDream.sln -c Release
dotnet test SomhairlesDream.sln -c Release --no-build
python3 tools/acceptance/e2e_acceptance.py --port 18201 \
  --bpython /tmp/bpyenv/bin/python
```

`--bpython` must point at a Python that can `import bpy` (see below); the
driver refuses to start without it.

## Server

```sh
dotnet run --project src/SomhairlesDream.Server
```

Environment variables (all optional):

- `SOMHAIRLES_ARTIFACT_ROOT` — artifact output root (default `.artifacts`)
- `SOMHAIRLES_STAGE_ROOT` — stage directory containing `blender/fixture.py`
  (default `stage`)
- `SOMHAIRLES_BPYTHON` — Python interpreter used to run the fixture
  (default `python3`)

Routes used by the frontend: `POST /api/runs/start`,
`GET /api/runs/events?projectId=...&runId=...` (run-scoped SSE),
`GET /api/artifacts/manifest`, `GET /api/artifacts/file`, and
`GET /api/artifacts/steps?projectId=...&runId=...&path=steps/<step>.glb`
(serve a step GLB while a run is still active; `.glb` files under `steps/`
only, traversal-guarded). The server binds to the ASP.NET default (add
`--urls http://127.0.0.1:8099` to match the legacy port).

## CLI

```sh
dotnet run --project src/SomhairlesDream.Cli -- run [options]
dotnet run --project src/SomhairlesDream.Cli -- verify <manifest.json>
```

`run` options: `--project-id`, `--run-id`, `--base-id`, `--target-id`,
`--output-root`, `--stage-root`, `--python`, `--render`. `SOMHAIRLES_BPYTHON`
sets the default interpreter. Exit codes: `0` success, `2` usage, `3` bridge
failure, `4` pipeline/verify failure. On success it prints the manifest path.

A run produces, under `<output-root>/<projectId>/<runId>/`:

- `events.ndjson`, `manifest.json`
- `logs/01-foundation.log`, `02-frame.log`, `03-cabin.log`
- `steps/01-foundation.glb`, `02-frame.glb`, `03-cabin.glb`

`verify` recomputes hashes, checks log presence, path containment, object-ID
proofs, timestamps, and duplicate steps without needing Blender.

## Python interpreter for bpy

The fixture requires a Python that can `import bpy` (Blender as a module,
version >= 3.0.0). A system `python3` usually cannot. This project was
verified with a uv-managed environment at `/tmp/bpyenv/bin/python`
(`bpy 5.0.1`). Note that `/tmp` is ephemeral; recreate or point
`SOMHAIRLES_BPYTHON`/`--python` at any interpreter that provides `bpy`.

Example:

```sh
dotnet run --project src/SomhairlesDream.Cli -- run \
  --output-root .artifacts --python /tmp/bpyenv/bin/python
```

## Fixture limitations

- `stage/blender/fixture.py` is a deterministic placeholder geometry fixture
  (named cubes with `object_id` custom properties), not a production model.
- Bridge timeout is 120 seconds per step; the bridge requires the fixture to
  write its result JSON to `--result-output` (stdout/stderr are diagnostics
  only, retained as step logs).
- Renders are produced only with `--render` (512x512 PNG per step).
- Reimport verification counts objects (`2 -> 5 -> 7` across the three steps)
  and checks the `object_id` custom properties survive the GLB round-trip.

## Verification status

- Full solution test suite and Release build: clean (0 warnings/errors).
- Real CLI pipeline with `/tmp/bpyenv/bin/python`: 3 GLBs + logs + manifest,
  `verify` exit 0.
- Browser end-to-end (playwright + bundled chromium, `--enable-unsafe-swiftshader`),
  server on `:8099` with the real bpy interpreter, run started through the
  page form: pre-run page shows exactly `当前没有下一版正在开发` and status
  `待机`; during the run the SSE snapshot drives the viewport to load
  `steps/01-foundation.glb` (status `运行中`, note `管线几何 · …` — LIVE
  ingestion, not replay); at completion the viewport shows
  `steps/03-cabin.glb`, status `已完成`, manifest link exposed; the served
  `steps/03-cabin.glb` is byte-identical (sha256) to the CLI-produced
  reference run; a failure run (pre-existing run directory) shows status
  `失败` with the error surfaced in `run-message`; zero console errors on
  all pages.
- Browser acceptance driver (see above), 45/45 checks green on desktop and
  narrow viewports: framing asserted numerically from the projected scene
  bounding box (not by trusting whole-canvas hashes alone), frozen
  screenshots byte-deterministic, GLB hash equality, failure surfacing,
  zero console errors. The frontend refits the camera to the loaded model
  (`THREE.Box3`-based framing) whenever the canvas resizes, and a resize
  never reverts the viewport to replay placeholder geometry.
- NOT verified: production deployment of the old site remains untouched;
  `--render` PNG renders; replay button flow against a historical run.