Preserve source locations across cached imports and record MVP acceptance

This commit is contained in:
Emil
2026-09-18 05:55:29 +03:00
parent 0f34b03631
commit 4cb82556de
14 changed files with 797 additions and 6 deletions
+2
View File
@@ -47,6 +47,8 @@ Entry function плагина согласует версию интерфейс
`TypeId/FieldId` описывают смысл сохраняемых данных и не зависят от имени C++, offset, порядка членов или `typeid`. Layout fingerprint нужен для совместимости конкретной сборки. Переименование сохраняет ID; изменение смысла/единиц требует миграции. Отсутствующий плагин не уничтожает неизвестные записи. Lua позже использует явно экспортированный runtime API и проверяемые handles; наличие metadata не делает каждый native-метод доступным MCP или Lua.
MVP доставляет декларативные шаги миграции через тот же schema manifest: `default`, `scale` и `require_manual`. Вся схема и правила проверяются до публикации Player/schema generation. Старые версии открываются как opaque data; миграция выполняется явно командой `component.migrate` с revision, одним Undo и записью recovery. Inherited component изменяется в исходной сцене; sparse overrides других экземпляров требуют явного обзора при изменении смысла или единиц. Произвольный C++ migration callback в Editor не загружается. Контракт и проверяемый пример — в [Manual](manual/scripting/api.md#editor-data-migrations).
## 5. Авторские данные, сцены и редактор — принято
Пользователь работает со сценами, объектами, компонентами и ресурсами. Сцены допускают **вложенные экземпляры шаблонов**. JSON хранит ссылку на исходный шаблон и **sparse overrides** — только локальные отличия, с устойчивыми адресами полей/объектов. Подписи и пути не являются идентичностью. Для добавления, удаления и изменения вложенного содержимого нужны явные операции; потерянная цель override становится диагностируемым конфликтом, а не молчаливым удалением данных.
+26
View File
@@ -261,3 +261,29 @@ including the launcher fix, and proceeded to real native Release exports. That r
predates checkpoint 5's new authoring/diagnostic changes, whose Windows checks remain
separate. The next full Windows build enables the optional diagnostic module too.
No MVP tag is claimed at this checkpoint.
## Checkpoint 5 acceptance and source relocation correction
The clean `0f34b03` Linux checkout built offline with 20/20 CPU tests, then created
and rendered a fresh project using the Manual's command. Both checked-in games were
exported in Release, moved outside the SDK into Unicode paths and run for 120 frames
with source projects hidden; Khronos validation was active with no errors. All seven
recovery scenarios and the real Blender/live-Editor round-trip passed. Exact inputs,
hashes and limits are in [the Linux acceptance record](validation/checkpoint5-linux-2026-09-18/README.md).
A subsequent source-relocation regression was reproduced and corrected: a cache-hit
rename/move updated the logical source pointer while retaining the historical payload
path, so the new freshness check incorrectly kept the asset stale. Publication now
stores both paths atomically. Earlier pointers remain readable by resolving the
payload relative to the moved logical source; immutable content generations and cache
keys are unchanged. Regression cases cover PNG, Blender bundles and external glTF
buffers/images, with both new and legacy pointer records. Full Linux integration
remains 34 passed / 1 explicit Wayland skip / 0 failed; targeted ASan/UBSan asset/cook
checks passed 2/2 after this correction.
Windows run `35299805623` at `e0b9651` completed successfully, including both Release
integration exports, incremental C++ rebuilding, and the two checked-in games with
Unicode relocation. It used SwiftShader without the Khronos validation layer and is
functional software-Vulkan evidence, not a physical-GPU benchmark. Checkpoint 5's
newer Windows graphics/export run and the relocation correction have their own
revision-specific gates.
+1 -1
View File
@@ -2,11 +2,11 @@
These files preserve bounded checks and their inputs. Each record states its source revision or working-tree limitation; a passing record does not certify later commits or every supported platform.
- [Checkpoint 5 Linux acceptance](checkpoint5-linux-2026-09-18/README.md): clean offline source build, first Editor launch, exact-candidate standalone games and live Blender checks.
- [Linux offline build](offline-linux-2026-09-18.json): a fresh build from committed `d834cfad67cd81d8c4998b90c16791361ca8c0f8`, prefetched dependency inputs, and a network namespace without external connectivity. CPU tests ran; graphics execution is separate.
- [Linux Release export and rendering](linux-release-2026-09-18/README.md): two relocated playable games, Unicode paths, imported Blender geometry, raw frame profiles and exact package hashes. This measurement used an evolving working tree around `5ac4db4`, not a clean checkout of the current commit.
- [Live Blender reimport](blender-live-linux-2026-09-18.json): actual Blender 4.5.3 exports and two live Editor instances, preserving authoring/gameplay/physics while refreshing geometry.
- [Editor recovery](editor-recovery-linux-2026-09-18.json): killed Editor, explicit journal recovery, failed save/import/build and failed Player startup with last-good preservation.
- [Native window boundaries](native-window-linux-2026-09-18.json): XWayland lifecycle, isolated Unicode clipboard, SDL text-input rectangles and the explicit native Wayland restore limitation.
- [Linux native window and input boundaries](native-window-linux-2026-09-18.json): XWayland resize/minimize/restore and SDL text-input-area checks; isolated Xvfb Unicode clipboard roundtrip with restoration. Native Wayland restore remains unconfirmed; real OS IME composition was not tested.
The Linux records do not establish Windows correctness, physical Windows GPU performance, native Wayland restore support, or editor responsiveness under large workloads. Windows CI artifacts record their own commit and software Vulkan results. A skipped compositor operation is not a completed acceptance check.
@@ -0,0 +1,38 @@
# Checkpoint 5 Linux acceptance
These functional checks used the engine source at
`0f34b036313c011861dbfd5828ed45c4f7940b05`. They supplement the earlier Release
performance baseline; export runs overlapped compilation and are not benchmarks.
- [Clean offline build](offline.json): committed source extracted with `git archive`,
empty build directory, external networking disabled, prefetched dependencies and
toolchain. Full default native build and 20 CPU tests passed in 205.77 seconds.
- [First Editor launch](first-run.json): that freshly built default Editor created
a new 3D project following the Manual's command, copied the correct gameplay
templates and rendered three native X11/Xvfb frames before closing successfully.
- [Both standalone games](playable-exports.json): the real Editor produced Release
packages, verified package hashes, moved them outside the SDK into Unicode paths,
hid source projects, then validated and rendered 120 frames per game without
scene/asset overrides. RTX 2080 Ti, Khronos validation active, zero validation
errors. The packages contained 17 files for 2D and 22 for 3D.
- [Blender round-trip](blender.json): unmodified Blender 4.5.3 and the actual live
Editor probe updated two instances while preserving IDs, transforms, tint, physics,
opaque gameplay and document revision. Removed outputs kept the last good visible
generation. The verification harness was updated to distinguish source freshness
from active-generation preservation; its separate hash is included in the report.
- [Editor recovery](recovery.json): seven failure/recovery scenarios passed against
the same source revision and recorded Editor hash: interrupted process, explicit
journal recovery, failed save/import/build/Player startup, and subsequent recovery.
The integrated build with optional diagnostics also passed 34 tests with one
explicit native Wayland restore skip; the sanitizer configuration passed 18/18.
These results describe Linux. Windows has separate CI and export evidence.
Native OS IME composition and moving between physical monitors were not exercised;
widget composition, DPI and native SDL input-boundary tests have narrower scope.
The following image is an actual Editor capture from the Blender probe, after
rename/geometry reimport. It is not the generated UI reference. The test scene has
two placed instances and no selected object.
![Native Faset Editor after Blender reimport](editor-blender.png)
@@ -0,0 +1,70 @@
{
"source_commit": "0f34b036313c011861dbfd5828ed45c4f7940b05",
"recorded_at_utc": "2026-09-18T02:45:37.886125+00:00",
"platform": "Linux-7.0.0-31-generic-x86_64-with-glibc2.43",
"working_tree_diff": [
" M tools/verify_blender_roundtrip.py"
],
"candidate_binaries_note": "Existing integrated Debug binaries from checkpoint5 build; SHA256 captured after this run. Engine source unchanged; verification harness has explicit freshness assertions patched after candidate commit.",
"command": "python3 tools/verify_blender_roundtrip.py --blender .cache/blender/blender --editor build/linux-debug/faset_editor --ui-probe build/linux-debug/faset_blender_editor_probe --output .cache/blender-final-candidate/verified",
"hashes": {
"build/linux-debug/faset_editor": "04baf2ec09f3f097657553d5b862ba5c90749d2cdbf4a239c6b92a45dee56ee9",
"build/linux-debug/faset_blender_editor_probe": "141433f89ae20202a16597525bfdcd59586bf17ab72ada29104ca324857b5842",
".cache/blender/blender": "5e15447670804c855fef6033ac9b8fd82696b762095becf0df79b1c2f8034397",
"tools/verify_blender_roundtrip.py": "ce3bb9d02500a6fe8becef23a7c966449a13d7d9d00d92f2177c0c431ac7f930",
"tests/blender/generate_fixture.py": "6d7951c0d35b8e4e71b81644af31d66af4e395511d929452a178f81ba30e487b",
"tests/editor_blender_reimport_probe.cpp": "0474276d04e8c18c1d2d8d55ff2cff8cfabba815bb3428af51394e22649e3923",
"tools/blender_addon/__init__.py": "e1a44aed99e09a6ad208966eb2e3808110d87311f5721774575a14471ed852e2",
"tools/blender_addon/bundle.py": "f0fd9707b853662be00d08440ab9cc65928cec5a8e053561834bd64fd43d1488",
"dependencies.lock.json": "0583b24061dfc6ba3db76be89ceeaf408cac18d237773833204074af5d8a81d5",
".cache/blender-final-candidate/verified/fixture/initial/manifest.json": "830818ee07a003aad63ed1345b0179852b9de1ab9a322142bd23e50fb501ba7d",
".cache/blender-final-candidate/verified/fixture/removed/manifest.json": "45e07ec2c4ae98c6a9b74a1b60089a2da1ff2ade1b9822d048b33de640d80665",
".cache/blender-final-candidate/verified/fixture/renamed/manifest.json": "dfbe56099ec026eaedd4a25218f3e3270ac075a933d77d37f8b784963792250c",
".cache/blender-final-candidate/verified/fixture/initial/payload/81fdfbd230989c9f4985963cddf63edcfb89c13a0ef4a9265c853006830be488.glb": "81fdfbd230989c9f4985963cddf63edcfb89c13a0ef4a9265c853006830be488",
".cache/blender-final-candidate/verified/fixture/removed/payload/3a9c9671bc1804454227d14f38705b9deacfd95492bf0aea1d2754ba198f128b.glb": "3a9c9671bc1804454227d14f38705b9deacfd95492bf0aea1d2754ba198f128b",
".cache/blender-final-candidate/verified/fixture/renamed/payload/568624163d90cf20f71629ca1cb11f128898260bc521ae66bfc1a3ae6bf4b3e6.glb": "568624163d90cf20f71629ca1cb11f128898260bc521ae66bfc1a3ae6bf4b3e6",
".cache/blender-final-candidate/verified/live-editor/initial.ppm": "363273f070f212580088d52b273baf94cebd62410bb9cf775dee14792f18bd99",
".cache/blender-final-candidate/verified/live-editor/renamed.ppm": "973a99aa43d4e32345bba023ec34dd443db9dc747590c128f996f5f258fd9038",
".cache/blender-final-candidate/verified/live-editor/initial.png": "57313599964308512499eb94d35d8711ff230ec6e8f4542802a147741efc77d4",
".cache/blender-final-candidate/verified/live-editor/renamed.png": "7df8f30b892f58af00ebfe48db3ddfb58150c1faffa19fa2fc792e3e65e8cfe1"
},
"results": {
"blender": "Blender 4.5.3 LTS",
"asset_id": "54c039e4-c19f-4365-a9f8-a38416ec6e3f",
"initial_generation": "499f254ce440ecb86118f2f6803f21013e763d35cfdba1c4356be7a457eec718",
"renamed_generation": "797e63652213752dc7c92029c4e9743517c5f40547b97eac748e60b17f142926",
"stable_output_ids": true,
"removed_output_conflict": true,
"failed_import_keeps_generation": true,
"authoring_preserved": true,
"changed_source_marked_stale": true,
"invalid_bundle_marked_stale": true,
"live_editor": {
"conflict_keeps_visible_generation": true,
"instances": 2,
"live_geometry_updated": true,
"opaque_gameplay_preserved": true,
"physics_fields_preserved": true,
"placement_and_tint_preserved": true,
"status": "passed",
"validation_enabled": true,
"validation_errors": 0
}
},
"limits": [
"Linux offscreen Vulkan Editor snapshot with validation; not a physical desktop input test.",
"No native OS IME or physical mixed-monitor DPI exercised.",
"Static geometry/material fixture; no skeleton/animation or arbitrary Blender shader fidelity claim.",
"Windows export and native platform acceptance are separate."
],
"original_harness_failure": "Whole faset_assets response equality was invalidated by new freshness current->stale; immutable active manifests remain equal. Revised harness separately verifies stale bundle.changed/bundle.unavailable and last-good generation.",
"hardware_context": {
"host_gpu": "NVIDIA GeForce RTX 2080 Ti",
"nvidia_driver": "595.84"
},
"screenshot": {
"path": ".cache/blender-final-candidate/verified/live-editor/renamed.png",
"stage": "After successful stable-ID rename and geometry reimport, before removed-output conflict.",
"conversion": "PNG encoded from renderer-produced renamed.ppm without resizing, cropping, recoloring or content changes."
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

@@ -0,0 +1,20 @@
{
"source_commit": "0f34b036313c011861dbfd5828ed45c4f7940b05",
"recorded_at_utc": "2026-09-18T02:50:10.534440+00:00",
"platform": "Linux-7.0.0-31-generic-x86_64-with-glibc2.43",
"source": "Fresh offline git archive checkout, default options, no previous build directory",
"command": "xvfb-run -a <clean-sdk>/build/linux-debug/faset_editor --project <new-directory>/MyGame --new MyGame --dimension 3 --frames 3 --capture <evidence>/editor.ppm",
"editor_sha256": "71182902617d617749d383ff87d29929013751b9597dcdbcb5f25ce323ebb64b",
"checks": {
"exit_code": 0,
"project_name": "MyGame",
"dimension": 3,
"gameplay_files_match_templates": true,
"rendered_capture": [
1440,
900
],
"capture_sha256": "e15c1e50106a4a188c817e9acb6382f13f094f163768a937ae39ddee3637971b"
},
"scope": "Manual build prerequisites already installed; engine built from clean source. Three native X11/Xvfb Editor frames; GPU execution does not prove physical OS IME or mixed-monitor behavior."
}
@@ -0,0 +1,12 @@
{
"format": "faset.offline-build",
"version": 1,
"revision": "0f34b036313c011861dbfd5828ed45c4f7940b05",
"recorded_at_utc": "2026-09-18T02:47:01.536454+00:00",
"seconds": 205.76940188600202,
"returncode": 0,
"network": "fresh user/network namespace; external connection must fail",
"source": "git archive of committed HEAD; no existing build directory",
"inputs": "prefetched archives + Slang; system compiler, SDK and development libraries",
"tests": "full native build, CPU CTests; GPU/window execution verified separately"
}
@@ -0,0 +1,452 @@
{
"format": "faset.playable-export-verification",
"version": 1,
"started_utc": "2026-09-18T02:43:37.045153+00:00",
"platform": "linux",
"engine": "/home/emil/Desktop/Faset_Engine",
"editor": "/home/emil/Desktop/Faset_Engine/build/linux-debug/faset_editor",
"standalone_root": "/tmp/faset-playable-exports-25rumkwv",
"frames_per_game": 120,
"status": "passed",
"projects": [
{
"name": "collect-2d",
"dimension": 2,
"source_inputs": [
{
"path": ".gitignore",
"sha256": "53aa4d7124d4c8d93b8d7c4cb470b6c82b844e019a660d6243ffeadc50602de1"
},
{
"path": "README.md",
"sha256": "d6c35206b10ee28efd5cc02da77a3ce9b6e1f8ce085793150bbf2f23bd925d65"
},
{
"path": "Scenes/main.scene.json",
"sha256": "be48a6d21385e717b6c8ebf9a3b4322eebbeff6bb067428e089ab5214be79854"
},
{
"path": "Scripts/Gameplay.cpp",
"sha256": "6b581b24da613e1915819de0a0f0ea820dd3333efa62c4b479a9ea7ffdbc6bcc"
},
{
"path": "Scripts/Gameplay.hpp",
"sha256": "4121587182ed4f74901b1c0757c53daadbf309af1d6bb5dd87fef142130ccbbe"
},
{
"path": "project.faset.json",
"sha256": "36e2acd32210f5489372a2331346a3294ead2395f0713e3e5fd2da4e83263689"
}
],
"generation": "2bf52972-98ee-4379-8b7c-eb6f0dfca6a5",
"configuration": "Release",
"standalone_directory": "/tmp/faset-playable-exports-25rumkwv/Faset Café 世界/collect-2d",
"executable": "faset_player",
"package_file_count": 17,
"asset_generations": {},
"device": "NVIDIA GeForce RTX 2080 Ti",
"validation_enabled": true,
"validation_errors": 0,
"completed_frames": 120,
"summary_ms": {
"gpu": {
"max": 0.592896,
"min": 0.558112,
"p50": 0.55904,
"p95": 0.577952,
"samples": 120
},
"render_call": {
"max": 4.604147,
"min": 1.474391,
"p50": 1.713513,
"p95": 2.008409,
"samples": 120
},
"renderer_cpu": {
"max": 4.60056,
"min": 1.472197,
"p50": 1.710667,
"p95": 2.00375,
"samples": 120
},
"renderer_readback_cpu": {
"max": 2.634671,
"min": 0.395376,
"p50": 0.449368,
"p95": 0.596425,
"samples": 120
},
"simulation": {
"max": 0.394364,
"min": 0.133442,
"p50": 0.146897,
"p95": 0.212962,
"samples": 120
},
"snapshot": {
"max": 0.208042,
"min": 0.125827,
"p50": 0.134384,
"p95": 0.182785,
"samples": 120
},
"wall": {
"max": 5.039538,
"min": 1.763687,
"p50": 2.01393,
"p95": 2.375862,
"samples": 120
}
},
"capture": {
"width": 1280,
"height": 720,
"sampled_colors": 7,
"sha256": "f9d74067d87c1ebf30c6db372a26d93b7285791c8cb0307a24af5adb23bf201a"
},
"source_project_paths_unavailable": true,
"status": "passed"
},
{
"name": "collect-3d",
"dimension": 3,
"source_inputs": [
{
"path": ".gitignore",
"sha256": "53aa4d7124d4c8d93b8d7c4cb470b6c82b844e019a660d6243ffeadc50602de1"
},
{
"path": "Assets/exit-arch/create.py",
"sha256": "9c7291124fb1498a3f992ef74afc39cd98f9cc3e2ff4687c633bece85d580905"
},
{
"path": "Assets/exit-arch/manifest.json",
"sha256": "7a13347adc89761fb02ebeab3c58ba87f319cd62f53ec87a3bb61bcd066ffa55"
},
{
"path": "Assets/exit-arch/manifest.json.faset-import.json",
"sha256": "ef4eef28a18a0cc40a40df75db34c97faa5d783b891265f862633f3aec514afc"
},
{
"path": "Assets/exit-arch/payload/ee6c748a212905fc9c465cbe95c033e94b80be643624f4f240ad5353761205b3.glb",
"sha256": "ee6c748a212905fc9c465cbe95c033e94b80be643624f4f240ad5353761205b3"
},
{
"path": "Assets/exit-arch/source.blend",
"sha256": "ff2676bec97ab778e531e87a185ad716049ea3b05b4a4f0027a6345737735035"
},
{
"path": "README.md",
"sha256": "2df5ca4df47ab5d3d05d41508201296d4ad5ec876245f19f061767ade6a30e8f"
},
{
"path": "Scenes/main.scene.json",
"sha256": "61b65baf79e52f07ac386a7f98acf985a1b09e88770cdde37ef461dc94b9dcd7"
},
{
"path": "Scripts/Extensions/Beacon.hpp",
"sha256": "c60d56161eead16f541835053ad32ac3333383d83015709c060b7d45e0af5a97"
},
{
"path": "Scripts/Gameplay.cpp",
"sha256": "5d419786a6861d7f14ed4eca54634694d8cd43be5dbbd60a2c6790db665c3a6a"
},
{
"path": "Scripts/Gameplay.hpp",
"sha256": "4121587182ed4f74901b1c0757c53daadbf309af1d6bb5dd87fef142130ccbbe"
},
{
"path": "project.faset.json",
"sha256": "43d273b1054ec9069369073f54cc1fc7efb3d6b699dcd342c552a2dc2ef852ef"
}
],
"import": {
"asset_id": "5832763b-3ed0-44d6-9088-0b524f196a91",
"cache_hit": false,
"diagnostics": [],
"generation": "4a5bfcc1437a7b4d65c388da2beace0308cceda8e16503d08ce676260f21d5c2",
"manifest": {
"asset_id": "5832763b-3ed0-44d6-9088-0b524f196a91",
"files": [
{
"path": "meshes/mesh-fc069a07a87e0f831fd92efdbc4efa1f-0.fmesh",
"sha256": "98b38aca7d2786c58a2265ba4d1f2255123f03d31d947e239232cf55bfdc2c97",
"size": 928
},
{
"path": "meshes/mesh-0961e2a3f92977836a3aa80cab09277e-0.fmesh",
"sha256": "98b38aca7d2786c58a2265ba4d1f2255123f03d31d947e239232cf55bfdc2c97",
"size": 928
},
{
"path": "meshes/mesh-93b5b3e52cb6bb120fefb80e2d8e222b-0.fmesh",
"sha256": "3875c16ea628d4126373ebfe07816621fbad3961596a2fcd112f1eec0e378071",
"size": 928
}
],
"generation": "4a5bfcc1437a7b4d65c388da2beace0308cceda8e16503d08ce676260f21d5c2",
"importer": "faset-gltf-1/cgltf-1.15",
"input_key": {
"bundle_sha256": "7a13347adc89761fb02ebeab3c58ba87f319cd62f53ec87a3bb61bcd066ffa55",
"dependencies": {},
"importer": "faset-gltf-1/cgltf-1.15",
"settings": {},
"source": "ee6c748a212905fc9c465cbe95c033e94b80be643624f4f240ad5353761205b3",
"target_profile": "desktop-static-pbr-v1",
"toolchain": {
"cgltf": "360db1a95480fe102ae9c69b27c5d101167ff5ba",
"stb": "2c980bb59875b0d32144a71867fbdebb2f77cd20"
}
},
"kind": "scene",
"materials": [
{
"alpha_cutoff": 0.5,
"alpha_mode": "OPAQUE",
"base_color": [
0.2199999988079071,
0.28999999165534973,
0.3400000035762787,
1.0
],
"base_color_texture": -1,
"double_sided": true,
"emissive": [
0.0,
0.0,
0.0
],
"emissive_texture": -1,
"format": "faset.material",
"id": "material-bb63b094febbf90bb7049c9bcf7587bc",
"metallic": 0.07999999821186066,
"metallic_roughness_texture": -1,
"name": "Weathered stone",
"normal_texture": -1,
"occlusion_texture": -1,
"roughness": 0.800000011920929,
"unlit": false,
"version": 1
}
],
"meshes": [
{
"id": "mesh-fc069a07a87e0f831fd92efdbc4efa1f",
"name": "Cube.001",
"primitives": [
{
"material": 0,
"path": "meshes/mesh-fc069a07a87e0f831fd92efdbc4efa1f-0.fmesh"
}
]
},
{
"id": "mesh-0961e2a3f92977836a3aa80cab09277e",
"name": "Cube.002",
"primitives": [
{
"material": 0,
"path": "meshes/mesh-0961e2a3f92977836a3aa80cab09277e-0.fmesh"
}
]
},
{
"id": "mesh-93b5b3e52cb6bb120fefb80e2d8e222b",
"name": "Cube.003",
"primitives": [
{
"material": 0,
"path": "meshes/mesh-93b5b3e52cb6bb120fefb80e2d8e222b-0.fmesh"
}
]
}
],
"nodes": [
{
"id": "node-cec6e8cea80cf5a98ba3263a0156bee4",
"local_transform": [
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
1.4500000476837158,
1.2999999523162842,
1.0
],
"mesh": 0,
"name": "Left post",
"parent_id": "",
"stable_source_id": true
},
{
"id": "node-5f8a90a6c4d27020d20bd4fa81d1d958",
"local_transform": [
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
1.4500000476837158,
-1.2999999523162842,
1.0
],
"mesh": 1,
"name": "Right post",
"parent_id": "",
"stable_source_id": true
},
{
"id": "node-482463e486a8d1fd741340c653cd128f",
"local_transform": [
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
3.0,
0.0,
1.0
],
"mesh": 2,
"name": "Lintel",
"parent_id": "",
"stable_source_id": true
}
],
"outputs": [
"node-cec6e8cea80cf5a98ba3263a0156bee4",
"node-5f8a90a6c4d27020d20bd4fa81d1d958",
"node-482463e486a8d1fd741340c653cd128f",
"mesh-fc069a07a87e0f831fd92efdbc4efa1f",
"mesh-0961e2a3f92977836a3aa80cab09277e",
"mesh-93b5b3e52cb6bb120fefb80e2d8e222b",
"material-bb63b094febbf90bb7049c9bcf7587bc"
],
"payload_source": "/home/emil/Desktop/Faset_Engine/.cache/playable-final-candidate/Faset Café 世界/collect-3d/Assets/exit-arch/payload/ee6c748a212905fc9c465cbe95c033e94b80be643624f4f240ad5353761205b3.glb",
"schema_version": 1,
"settings": {},
"source": "/home/emil/Desktop/Faset_Engine/.cache/playable-final-candidate/Faset Café 世界/collect-3d/Assets/exit-arch/manifest.json",
"source_sha256": "ee6c748a212905fc9c465cbe95c033e94b80be643624f4f240ad5353761205b3",
"textures": []
},
"previous_generation": "",
"removed_output_ids": []
},
"generation": "b0a40126-7d6c-414f-843a-23a677ad6f8f",
"configuration": "Release",
"standalone_directory": "/tmp/faset-playable-exports-25rumkwv/Faset Café 世界/collect-3d",
"executable": "faset_player",
"package_file_count": 22,
"asset_generations": {
"5832763b-3ed0-44d6-9088-0b524f196a91": "4a5bfcc1437a7b4d65c388da2beace0308cceda8e16503d08ce676260f21d5c2"
},
"device": "NVIDIA GeForce RTX 2080 Ti",
"validation_enabled": true,
"validation_errors": 0,
"completed_frames": 120,
"summary_ms": {
"gpu": {
"max": 0.615584,
"min": 0.578944,
"p50": 0.58096,
"p95": 0.597312,
"samples": 120
},
"render_call": {
"max": 4.88184,
"min": 1.554402,
"p50": 1.842957,
"p95": 2.153072,
"samples": 120
},
"renderer_cpu": {
"max": 4.876761,
"min": 1.552489,
"p50": 1.838648,
"p95": 2.149254,
"samples": 120
},
"renderer_readback_cpu": {
"max": 2.787459,
"min": 0.398262,
"p50": 0.483592,
"p95": 0.577219,
"samples": 120
},
"simulation": {
"max": 0.38139,
"min": 0.16457,
"p50": 0.192834,
"p95": 0.278605,
"samples": 120
},
"snapshot": {
"max": 0.816561,
"min": 0.155042,
"p50": 0.172175,
"p95": 0.243289,
"samples": 120
},
"wall": {
"max": 6.073458,
"min": 1.902519,
"p50": 2.248081,
"p95": 2.61298,
"samples": 120
}
},
"capture": {
"width": 1280,
"height": 720,
"sampled_colors": 42,
"sha256": "914060401de93d08b33bf09a1e210859e491a0060c331e5e2044f517356f4980"
},
"source_project_paths_unavailable": true,
"status": "passed"
}
],
"finished_utc": "2026-09-18T02:47:27.268249+00:00",
"engine_commit": "0f34b036313c011861dbfd5828ed45c4f7940b05",
"editor_sha256": "04baf2ec09f3f097657553d5b862ba5c90749d2cdbf4a239c6b92a45dee56ee9",
"working_tree_clean_at_start": true,
"engine_commit_at_completion": "0f34b036313c011861dbfd5828ed45c4f7940b05",
"timings_are_benchmark_evidence": false,
"timing_note": "Functional export/relocation/validation/profile completeness acceptance; timings are not benchmark evidence because a separate build runs concurrently.",
"visual_inspection": {
"status": "passed",
"notes": [
"2D capture shows player, platforms, three collectibles, barrier and exit marker.",
"3D capture shows player, room, collectibles, platform, gate and imported exit structure without missing geometry."
],
"png_previews": [
"evidence/collect-2d.png",
"evidence/collect-3d.png"
]
}
}
@@ -0,0 +1,36 @@
{
"format": "faset.editor-recovery-verification",
"version": 1,
"status": "passed",
"checks": [
"acknowledged unsaved edit survives killed Editor and explicit recovery",
"failed save preserves live authoring and last saved file",
"failed Player process is reported without changing authoring data",
"Undo repairs the scene, then Play and Stop leave authoring unchanged",
"failed C++ build keeps prior schema and reports stale status",
"fixed C++ rebuild restores current schema status",
"failed import preserves authoring"
],
"recorded_at_utc": "2026-09-18T02:50:13.733355+00:00",
"platform": "Linux-7.0.0-31-generic-x86_64-with-glibc2.43",
"editor_sha256": "04baf2ec09f3f097657553d5b862ba5c90749d2cdbf4a239c6b92a45dee56ee9",
"logs": [
"build completed",
"Gameplay schema loaded",
"Play started in a separate Player process",
"Player failed: unsupported builtin component version: faset.transform\n",
"Player exited with code 1",
"build completed",
"Gameplay schema loaded",
"Play started in a separate Player process",
"New round: collect the three gold cubes, then reach the green exit. E resets.\n{\"device\":\"NVIDIA GeForce RTX 2080 Ti\",\"dimension\":2,\"frames\":411,\"ticks\":104,\"validation_errors\":0}\n",
"Play stopped; authoring scene unchanged",
"build failed: Process exited with code 1; see job log; previous gameplay metadata remains available and is marked stale",
"build completed",
"Gameplay schema loaded",
"Asset import failed: Cannot read: /home/emil/Desktop/Faset_Engine/.cache/editor-recovery-final-candidate/project/Assets/missing.glb\n"
],
"engine_commit": "0f34b036313c011861dbfd5828ed45c4f7940b05",
"engine_commit_at_completion": "0f34b036313c011861dbfd5828ed45c4f7940b05",
"source_note": "Completed before pending moved-input freshness changes to asset_data.cpp/asset_pipeline.cpp; final-candidate committed engine source."
}
+21 -1
View File
@@ -133,7 +133,27 @@ Json AssetStore::current_manifest(const std::string& id) const {
if (manifest.at("asset_id").get<std::string>() != id ||
manifest.at("generation").get<std::string>() != generation)
throw std::runtime_error("Cooked manifest identity does not match its generation");
// One pointer snapshot prevents mixing two concurrently published generations.
// Locations belong to the active pointer, not the immutable content recipe:
// moving sources can reuse an existing generation whose paths are historical.
// One pointer snapshot keeps logical source and payload in the same publication.
if (pointer.contains("payload_source"))
manifest["payload_source"] = pointer.at("payload_source");
else {
// Earlier pointers only stored source. Ordinary files are their own
// payload; a bundle payload keeps its path relative to the manifest.
const auto previous_source =
faset::path_from_utf8(manifest.at("source").get<std::string>());
const auto previous_payload = faset::path_from_utf8(
manifest.value("payload_source", faset::path_to_utf8(previous_source)));
const auto current_source = faset::path_from_utf8(pointer.at("source").get<std::string>());
const auto current_payload =
previous_payload == previous_source
? current_source
: (current_source.parent_path() /
previous_payload.lexically_relative(previous_source.parent_path()))
.lexically_normal();
manifest["payload_source"] = faset::path_to_utf8(current_payload);
}
manifest["source"] = pointer.at("source");
return manifest;
}
+2 -1
View File
@@ -828,7 +828,8 @@ ImportResult AssetPipeline::import_asset(const ImportRequest& request, ImportJob
throw Cancelled{};
atomic_json(asset_root / "current.json", {{"schema_version", 1},
{"generation", result.generation},
{"source", faset::path_to_utf8(logical_source)}});
{"source", faset::path_to_utf8(logical_source)},
{"payload_source", faset::path_to_utf8(source)}});
result.status = ImportStatus::succeeded;
job.report(1, "complete");
} catch (const Cancelled&) {
+99
View File
@@ -101,6 +101,95 @@ void success(const ImportResult& result) {
throw std::runtime_error(text);
}
}
void relocated_inputs(AssetPipeline& pipeline, const fs::path& root) {
const auto before = root / faset::path_from_utf8("Источники до переноса");
const auto after = root / faset::path_from_utf8("Источники после переноса");
fs::create_directories(before / "payload/nested");
glb(before / "payload/nested/mesh.glb", "Moved bundle");
const Json bundle{
{"schema_version", 1},
{"asset_id", "relocated-bundle"},
{"files",
Json::array({{{"path", "payload/nested/mesh.glb"},
{"sha256", faset::sha256_file(before / "payload/nested/mesh.glb")}}})}};
save(before / "manifest.json", bundle.dump());
const auto first = pipeline.import_asset({before / "manifest.json"});
success(first);
const auto immutable = pipeline.generation_directory(first.asset_id) / "manifest.json";
const auto original_digest = faset::sha256_file(immutable);
fs::rename(before, after);
const auto moved = pipeline.import_asset({after / "manifest.json"});
success(moved);
require(moved.cache_hit && moved.generation == first.generation &&
moved.asset_id == first.asset_id,
"Bundle directory relocation must reuse the content generation and identity");
require(pipeline.freshness(first.asset_id).at("state") == "current" &&
pipeline.current_manifest(first.asset_id).at("payload_source") ==
faset::path_to_utf8(after / "payload/nested/mesh.glb"),
"Moved bundle freshness must inspect the current payload location");
require(faset::sha256_file(immutable) == original_digest,
"Relocation must not rewrite an immutable generation manifest");
// Earlier pointers only recorded logical source; they must remain readable
// after a cache-hit relocation without consulting missing old source files.
const auto pointer = pipeline.cache_root() / "assets" / first.asset_id / "current.json";
auto old_pointer = faset::read_json(pointer);
old_pointer.erase("payload_source");
faset::atomic_write_json(pointer, old_pointer);
require(pipeline.freshness(first.asset_id).at("state") == "current" &&
pipeline.load_asset(first.asset_id).generation == first.generation,
"Legacy bundle pointer must resolve payload relative to the moved source");
glb(after / "payload/nested/mesh.glb", "Moved bundle", true, false, .8f);
require(pipeline.freshness(first.asset_id).at("state") == "stale" &&
pipeline.load_asset(first.asset_id).generation == first.generation,
"Changed relocated bundle payload must be stale without changing cooked data");
const auto external_before = root / "external-before";
const auto external_after = root / faset::path_from_utf8("Внешний glTF после переноса");
fs::create_directories(external_before / "deps");
auto gltf = document("Moved external", true, false);
gltf["buffers"][0]["uri"] = "deps/geometry.bin";
gltf["images"] = Json::array({{{"uri", "deps/picture.png"}, {"mimeType", "image/png"}}});
gltf["textures"] = Json::array({{{"source", 0}}});
gltf["materials"][0]["pbrMetallicRoughness"]["baseColorTexture"] = {{"index", 0}};
save(external_before / "model.gltf", gltf.dump());
save(external_before / "deps/geometry.bin", geometry(0));
save(external_before / "deps/picture.png", faset::test_images::png_red_green);
const auto external = pipeline.import_asset({external_before / "model.gltf"});
success(external);
const auto external_immutable =
pipeline.generation_directory(external.asset_id) / "manifest.json";
const auto external_digest = faset::sha256_file(external_immutable);
fs::rename(external_before, external_after);
const auto relocated = pipeline.import_asset({external_after / "model.gltf"});
success(relocated);
require(relocated.cache_hit && relocated.generation == external.generation &&
relocated.asset_id == external.asset_id &&
pipeline.freshness(external.asset_id).at("state") == "current",
"Moving glTF and external dependencies together must preserve a current cache hit");
const auto external_pointer =
pipeline.cache_root() / "assets" / external.asset_id / "current.json";
auto legacy = faset::read_json(external_pointer);
legacy.erase("payload_source");
faset::atomic_write_json(external_pointer, legacy);
require(pipeline.freshness(external.asset_id).at("state") == "current",
"Legacy ordinary glTF pointer must treat the current source as payload");
save(external_after / "deps/geometry.bin", geometry(.4f));
auto freshness = pipeline.freshness(external.asset_id);
require(freshness.at("state") == "stale" &&
freshness.at("reasons")[0].at("code") == "dependency.changed" &&
freshness.at("reasons")[0].at("path") ==
faset::path_to_utf8((external_after / "deps/geometry.bin").lexically_normal()),
"Freshness must hash external geometry at its new location after directory move");
save(external_after / "deps/geometry.bin", geometry(0));
save(external_after / "deps/picture.png", faset::test_images::png_blue_white);
freshness = pipeline.freshness(external.asset_id);
require(freshness.at("state") == "stale" &&
freshness.at("reasons")[0].at("path") ==
faset::path_to_utf8((external_after / "deps/picture.png").lexically_normal()),
"Freshness must inspect moved external images too");
require(faset::sha256_file(external_immutable) == external_digest,
"Freshness/relocation must leave the published glTF generation immutable");
}
} // namespace
int main() {
const auto root =
@@ -410,6 +499,16 @@ int main() {
"Image rename retained old source pointer");
require(pipeline.overrides(image_first.asset_id).contains(texture_id),
"Image rename/reimport lost overrides");
require(pipeline.freshness(image_first.asset_id).at("state") == "current",
"Renamed and reimported PNG must be current, not stale at its old payload path");
const auto image_pointer =
pipeline.cache_root() / "assets" / image_first.asset_id / "current.json";
auto old_image_pointer = faset::read_json(image_pointer);
old_image_pointer.erase("payload_source");
faset::atomic_write_json(image_pointer, old_image_pointer);
require(pipeline.freshness(image_first.asset_id).at("state") == "current",
"Legacy image pointer must follow the renamed logical source");
relocated_inputs(pipeline, root);
const auto jpeg = root / "sprite.JPEG";
save(jpeg, faset::test_images::jpeg_red_green);
auto jpeg_import = pipeline.import_asset({jpeg});
+18 -3
View File
@@ -50,20 +50,35 @@ def verify(blender, editor, output, ui_probe=None):
def ids(value):
return sorted(value["outputs"])
assert ids(manifest) == ids(renamed["result"]["manifest"]), (manifest, renamed)
def active_manifests(assets):
return {item["id"]: item["manifest"] for item in assets["assets"]}
def freshness(assets):
return next(item["freshness"] for item in assets["assets"] if item["id"] == identity)
active = command("faset_assets", {})
assert freshness(active)["state"] == "current", active
removed = import_stage("removed")
assert removed["state"] == "conflict", removed
assert command("faset_assets", {}) == active
after_removal = command("faset_assets", {})
# Source freshness must change without replacing the last-good generation.
assert active_manifests(after_removal) == active_manifests(active)
assert freshness(after_removal)["state"] == "stale", after_removal
assert any(reason["code"] == "bundle.changed"
for reason in freshness(after_removal)["reasons"]), after_removal
(source / "manifest.json").write_text('{"broken":true}')
failed = command("faset_import", {"path": "Assets/door/manifest.json"})
assert failed["state"] == "failed", failed
assert command("faset_assets", {}) == active
after_failure = command("faset_assets", {})
assert active_manifests(after_failure) == active_manifests(active)
assert freshness(after_failure)["state"] == "stale", after_failure
assert any(reason["code"] == "bundle.unavailable"
for reason in freshness(after_failure)["reasons"]), after_failure
assert scene_file.read_bytes() == before
report = {"blender": subprocess.check_output([str(blender), "--version"], text=True).splitlines()[0],
"asset_id": identity, "initial_generation": first["result"]["generation"],
"renamed_generation": renamed["result"]["generation"],
"stable_output_ids": True, "removed_output_conflict": True,
"failed_import_keeps_generation": True, "authoring_preserved": True}
"failed_import_keeps_generation": True, "authoring_preserved": True,
"changed_source_marked_stale": True, "invalid_bundle_marked_stale": True}
if ui_probe:
probe = subprocess.run([str(ui_probe), str(fixture), str(output / "live-editor")],
capture_output=True, text=True, encoding="utf-8")