Project SDK Workflow — Implementation Evidence Ledger¶
This ledger records the actual commands, results, environment facts and
limitations observed while implementing .kiro/specs/project-sdk-workflow. It is
the honest record required by requirement P15 and the per-phase evidence rule
in tasks.md. Where a required gate could not be executed in this environment it
is recorded as UNAVAILABLE with the reason, never as passed.
Starting point¶
- Starting revision:
6065bf505cfdb9ed619db30586da1b71031135e8(docs: design independent game projects and shared SDK workflow). - Implementation branch:
codex/project-sdk-workflow(created frommain). - Working tree at branch creation: clean (
git status --shortempty); no unrelated local changes to preserve beyond the committed design package.
Environment / toolchain audit (P0)¶
The repository's pinned reference toolchain is Clang/LLVM 18 + LLD 18 on
Ubuntu 24.04, with managed CMake 3.29.6, Ninja 1.11.1.3 and Conan 2.8.1
(config/tool_versions.json), plus the pinned Slang 2026.1.2 / spirv-val shader
tools and (for the optional Editor) Qt6.
Actual tools present in this sandbox (<tool> --version):
| Tool | Required (pinned) | Present here | Usable for gates? |
|---|---|---|---|
| clang / clang++ | 18.x + LLD 18 | 15.0.7 (clang version 15.0.7) |
No — wrong major, no LLD 18 |
| cmake | 3.29.6 managed | 3.22.2 system | No — below cmake_minimum_required(3.29) |
| ninja | 1.11.1.3 | 1.10.2 | Partial |
| conan | 2.8.1 | not installed | No |
| clang-format | 18.0.0 | 23.1.1 | No — pinned 18 required; newer reports non-18 checks |
| clang-tidy | 18.0.0 | (LLVM, non-18) | No |
| python3 | >= 3.10 | 3.9 default; 3.11.15 available via pyenv | Yes (use 3.11.15) |
| Qt6 | required for Editor | not installed | No |
| Slang / spirv-val | pinned 2026.1.2 | not installed | No |
| git / gh | — | 2.50.1 / 2.95.0 | Yes |
Consequences for acceptance (recorded honestly, per P15):
- UNAVAILABLE in this sandbox: native engine build; full SDK build + install + relocation; external native consumer link; ASan/UBSan build+tests; pinned clang-format-18 / clang-tidy-18 gates; build-time budget (needs the pinned compiler); native Qt Editor GUI acceptance (New Project / Open / build / run / stop); browser/Emscripten and RAD regressions. These require the pinned toolchain, Conan network access to a working package set, a GPU/display, and Qt. They are deferred to a machine with the reference toolchain and are listed in the "Remaining gaps" section; the PR is therefore a draft.
- AVAILABLE here: the Python host-tooling layer (SDK store, archive
validation, descriptor/lock v2 schema, templates, resolution, CLI) runs on
Python 3.11.15 and is covered by
unittestsuites executed below; CMake export metadata review by inspection; schema/fixture consistency between C++ and Python by shared fixtures.
Per AGENTS.md Rule 0, these are reported as unavailable rather than satisfied from a version string. "Never claim binary compatibility from a version string alone" (handoff) is honored: SDK identity is derived from actual build inputs and these gates stay open until run on the reference toolchain.
Baseline test state (before implementation)¶
Pinned-toolchain gates could not be run (see table). The existing Python
unittest suites were run with the available 3.11.15 interpreter:
$ PY=/root/.pyenv/versions/3.11.15/bin/python3
$ cd scripts/python && $PY -m unittest test_editor_tool # Ran 35 tests OK
$ $PY -m unittest test_formatting # Ran 13 tests OK (skipped=13, clang-format-18 absent)
$ $PY -m unittest test_rad_debugger # Ran 22 tests OK
$ $PY -m unittest test_web_package # Ran 5 tests OK
So the Python baseline is green; test_formatting skips because pinned
clang-format-18 is absent (expected, pre-existing).
Compatibility / identity model (P03/P11)¶
SDK identity is derived from the actual CMake build inputs, not a version string:
- engine
version+source_revision(git revision of the producer tree), - target triple (OS/arch), compiler id + version, C++ runtime/ABI tag and distro baseline captured from the build environment,
flavor(Debug/Development/Profile/Release) and the existingLUDUS_SDK_VARIANTcompatibility key (assert policy version, dialogs, asan, ubsan, tsan),- sorted
features, assertionpolicyblock, packageformat, and the per-module/target + dependency inventory with their redistributable notices, - payload
digest(SHA256) stored in the release catalog / lock / sidecar, never recursively inside the bytes being hashed.
Compatibility is checked before configure; mismatched flavor/toolchain/policy is rejected with expected-vs-actual detail and never falls back to building the engine.
P1 — relocatable runtime SDK (implementation + what remains)¶
Implemented (reviewable by inspection + the Python checks noted):
cmake/LudusSdkManifest.json.inextended to schema 2: adds source revision, target triple/os/arch, compiler id/version,cxx_runtime_abi,distro_baseline, build type, sanitizer block, a nestedassert_policyblock, sortedfeatures,components, a redistributabledependenciesinventory (name/version/licenses/kind) andsystem_prerequisites. The legacy flatenable_asserts/break_on_check/assert_policy_version/assert_dialogs_available/build_flavor_id/sdk_variantfields are retained soengine.verify_sdk_installandcmake/CheckSdkVariant.cmake.inkeep working unchanged.cmake/EngineSdkIdentity.cmake(new): derives triple / C++ runtime ABI tag / distro baseline from the actual build inputs and accumulates the component, feature, dependency and system-prerequisite inventory via GLOBAL properties, serialized to the manifest@…_JSON@placeholders byludus_sdk_finalize_identity().- Root
CMakeLists.txt: computes compiler/source identity at root scope, registers the audited link-closure dependencies (volk bundled; FreeType + HarfBuzz bundled, consumed PRIVATE byLudus::Textbut still in the final consumer's static link line; Threads system), the system prerequisites (Vulkan loader, pthreads, Wayland when enabled, host Slang/spirv-val) and the components, then installsTHIRD_PARTY_NOTICES.mdand a stable package-local dependency search directorylib/cmake/Ludus/dependencies. cmake/LudusConfig.cmake.in: prepends the bundled dependency directory toCMAKE_PREFIX_PATHlocally (restored at the end so global consumer search state is never mutated), resolves volk/FreeType/HarfBuzz/Threads from the bundled metadata when present, exports the full identity asLudus_*vars, and adds a publicludus_apply_app_policy()helper (C++23 + no-exceptions) for templates so they never reference the checkout-onlyludus_apply_project_defaults.cmake/sdk/THIRD_PARTY_NOTICES.md(new): records bundled vs system dependencies and their obligations; the manifestdependenciesarray is the machine-readable twin.scripts/python/engine.pyverify_sdk_install: now also requires the notices file, audits ALL installedlib/cmake/Ludus/**.cmakefor producer-path leakage (not just the Ludus package files), and asserts the manifest identity fields are resolved (no@…@/unknown) with a non-empty dependency inventory.
Checks run here (Python 3.11.15):
$ cmake -P (EngineSdkIdentity triple/abi/distro + accumulators) # triple/abi derive OK;
# GLOBAL-property accumulation is NOT scriptable under `cmake -P` (CMake limitation),
# so the accumulator path is exercised only in a real configure (UNAVAILABLE here).
$ python -m unittest test_ludus_tools # 33 OK — includes ManifestTemplate rendering the
# extended template to valid JSON, parsing it into SdkIdentity, and asserting the
# legacy flat fields remain.
$ python -m unittest test_editor_tool # 35 OK — engine.py edits did not regress E0.
UNAVAILABLE (requires the pinned toolchain + Conan, deferred): the actual
cmake --install of the extended manifest; building Debug/Development/Release
candidate archives with hashes; populating lib/cmake/Ludus/dependencies with
the real bundled volk/FreeType/HarfBuzz CMake metadata + static libs; extracting
at a fresh prefix with no producer checkout/Conan cache and configuring/building/
running an external consumer that links every public module. These are the P1
relocation gate and remain open; the manifest/export/config plumbing and the
Python-side store/identity validation that consume them are implemented and
tested.
P2 / P3 — host tools, shared SDK store, portable project CLI¶
Implemented in the ludus_tools package (installable, stdlib-only, Qt-free):
templates.py: versioned bundledminimalnative template (v1); atomic no-replace creation — stage into a sibling dir, reject existing/symlink/ nonempty destinations, re-check and never overwrite a destination that appears between staging and publish, discard staging on cancellation.create.py:create_projectties descriptor + unresolved lock + rendered template;--sdkrecords a local prefix ONLY in ignored.ludus/local.jsonand keeps the committed lock unresolved (no fabricated hashes).migrate_v1_to_v2is explicit, preserves relative paths/launch settings, requires an engine selection, refuses providerludus, and does a recoverable paired descriptor+lock commit (.migratingtemporaries +recover_partial_migration).resolve.py: precedence = per-op--sdk→ saved override → locked release in the store; prints resolved identity/path/override; unresolved lock without an override is an actionable stop (never a fallback engine build); resolved-input stamp (manifest + exported cmake + lib size/mtime) gates reconfigure/relink for mutable local prefixes and aborts if the prefix changes mid-operation.buildlock.py: the per-build-tree cooperative lock uses the SAME lock path aseditor_tool.BuildTreeLockso CLI and Editor interlock (second =Busy).operations.py: synchronous configure/build/run with exact argv/no-shell, CMake File API artifact resolution via the existingcmake_targets.py, reconfigure-on-stamp-change, and the rule that a failed build refuses to launch a stale binary.cli.py+__main__.py+scripts/ludus: theludus sdk …/ludus project …commands with stable exit codes and--json.pyproject.tomlpackages a redistributable wheel exposing aludusentry point with no third-party runtime deps.
Checks run here (Python 3.11.15):
$ python -m unittest test_ludus_tools test_ludus_project_ops test_ludus_cli # 59 OK
$ python -m ludus_tools project create /tmp/MyGame --name "My Game" --engine 0.1.0 # creates v2 project
$ # two projects share one installed SDK, each with its own ignored override,
$ # committed locks unchanged (test_ludus_cli.test_two_projects_one_sdk)
$ python -m venv /tmp/ludus-venv && /tmp/ludus-venv/bin/pip install . # install at a NEW location
$ cd /tmp && /tmp/ludus-venv/bin/ludus --store /tmp/s sdk list # runs with no checkout on path
$ /tmp/ludus-venv/bin/python -c "import ludus_tools,sys; print([m for m in sys.modules if 'PyQt' in m or 'PySide' in m or m=='engine'])"
[] # no Qt/engine modules loaded — Qt-free, checkout-free (P07/P11)
Behaviors proven against a FAKE SDK (a tar of a minimal prefix + valid
manifest), since the real SDK build is UNAVAILABLE: archive traversal/absolute/
symlink rejection, digest mismatch, cancellation, reuse of an identical install,
no-replace creation + destination race, v1→v2 migration + partial-migration
recovery, resolution precedence, flavor-mismatch rejection, stamp-change
detection, and build with an unresolved lock exiting UNAVAILABLE rather than
building the engine.
UNAVAILABLE (needs the pinned native toolchain): actually configuring/
building/running the generated C++ project (the generated CMake requires 3.29
and Clang 18 + a real SDK), and inspecting its compile database to prove no
engine source compiles. The operations.py path that drives this is
implemented and unit-covered up to the cmake invocation; the compile itself is
deferred to the reference toolchain.
P4 — Editor v2 schema sharing (implementation + GUI gap)¶
Implemented:
apps/editor/src/internal/project_descriptor.h: addedVersion,EngineRequirement,TemplateRef, theHasEngine/HasTemplateflags and the version-2 limit constants (kept equal toludus_tools.descriptor).apps/editor/src/project_store.cpp:ParseDescriptornow accepts version 1 and 2. Version-dependent root keys preserve the old-reader rule (a v1 file still rejects v2's extra fields and the Release preset); v2 adds thelinux-clang-releasepreset, the required-for-cmake / forbidden-for-ludusengineobject (exact non-floating version, bounded unique components/ features) and the optionaltemplateobject.SerializeDescriptoris now version-aware and round-trips the v2 fields.- New shared v2 fixtures
apps/editor/tests/fixtures/cases_v2.json(+ the sixvalid_v2_*/invalid_v2_*files) consumed by BOTH readers. apps/editor/tests/project_store_v2_tests.cppruns the C++ reader againstcases_v2.jsonand a v2 serialize round-trip; wired intoludus_editor_tests.scripts/python/test_ludus_tools.pyruns the Python v2 reader against the samecases_v2.json. The legacy v1-onlyeditor_project.pyand itstest_editor_tool.pyfixture checks are untouched, so v1 behavior and the "old reader rejects v2" rule are both preserved.
Checks run here:
$ python -m unittest test_ludus_tools test_ludus_project_ops test_ludus_cli test_editor_tool # 95 OK
# includes DescriptorV1Compat.test_shared_v2_fixture_cases (Python v2 reader vs cases_v2.json)
# and test_legacy_reader_still_rejects_v2.
UNAVAILABLE (needs Qt6 + Clang 18, neither present): compiling
ludus_editor_tests to execute project_store_v2_tests.cpp; and all native Qt
GUI acceptance — the asynchronous New Project / SDK install+select stages, the
engine identity/profile/override controls, Editor↔CLI build-lock contention,
form-retention on failure, and the Editor ON vs independent OFF checks. Per the
handoff, native GUI acceptance cannot be replaced by offscreen tests, so it
is explicitly deferred to a reference-toolchain machine. The C++↔Python schema
sharing is implemented and cross-checked through the shared fixtures (the C++
half is verified only when the editor is compiled on that machine).
P2 (catalog) / P5 — release catalog, CI, docs, Sandbox follow-up¶
Implemented:
ludus_tools/catalog.py: publisher release-catalog model (HTTPS-only URLs, per-entry sha256) + bounded streamed download (declared size + hard cap, truncation + digest verification) that hands the archive to the strictSdkStore.install_archive.ludus sdk install --version … --catalog …wires it in. Remote metadata is fetched only on explicit install. A project descriptor can never supply a URL/script..github/workflows/project-sdk.yml: candidate-package CI (every push/PR): host-tooling unit tests on pinned Python + install-into-fresh-venv + Qt/engine- free assertion; then a Development SDK candidate build, relocation to a fresh dir with an empty environment (env -i, noout/conan, no checkout on the path), external-consumer link against the relocated prefix, and a compile-database check that no engine source is compiled. Never publishes..github/workflows/release.yml: version-tag assembly/validation for all three flavors with tag/version/revision agreement and resolved-identity + dependency-inventory assertions; apublishjob gated toworkflow_dispatchonly,environment: release, least-privilegecontents: write, and an explicit guard that refuses to publish from a PR. The publish step is a documented placeholder — this task must not publish a public release.docs/development/project-sdk-workflow.md: user guide (install, SDK install/ override/refresh, prerequisites, create, migrate, direct CMake, recovery, compatibility limits, Sandbox follow-up).
Checks run here (Python 3.11.15):
$ python -m unittest test_ludus_tools test_ludus_project_ops test_ludus_cli # 65 OK
# CatalogDownload: HTTPS-only parse, digest mismatch, truncation, oversize,
# and download->install happy path (injected local opener; no network).
$ python -c "import yaml; [yaml.safe_load(open(f)) for f in (...workflows...)]" # YAML valid
UNAVAILABLE / not performed (by design or environment):
- The
project-sdk.yml/release.ymljobs that need the pinned native toolchain + Conan + Wayland were authored but not executed here (no runner, no toolchain). They are standard GitHub-hosted-runner steps intended to run onubuntu-24.04with the reference toolchain. - No release is published, no tag created, no merge performed (task constraint).
Ludus-Sandbox (P14) — honest status¶
Ludus-Sandbox is not present in this checkout and was not inspected or
modified. Its conversion (remove the mandatory nested engine build; pin an engine
release; consume find_package(Ludus CONFIG REQUIRED) + the ludus CLI) is a
separate change in that repository requiring its own access, implementation and
evidence. This PR validates the standalone reference consumer in this repository
(tests/sdk_consumer + the generated minimal template) and records the Sandbox
conversion as an explicit external follow-up. A separate Sandbox PR is NOT a gate
for this Ludus PR, but the pending conversion is clearly recorded here and in
docs/development/project-sdk-workflow.md.
Summary of checks run in this sandbox¶
$ cd scripts/python && python3.11 -m unittest \
test_ludus_tools test_ludus_project_ops test_ludus_cli \
test_editor_tool test_formatting test_rad_debugger test_web_package
Ran 140 tests ... OK (skipped=13) # 13 skips = clang-format-18 absent (pre-existing)
- New host-tooling suites:
test_ludus_tools(schema/store/identity/manifest/ catalog),test_ludus_project_ops(templates/create/migrate/resolve),test_ludus_cli(end-to-end CLI incl. two-projects-one-SDK, override set/clear, unresolved-lock→UNAVAILABLE, migration). - Legacy
editor_project.py,editor_tool.py,cmake_targets.pyare unchanged (git diff --stat mainempty), so v1 descriptor parsing and the E0 streaming protocol/supervision are preserved.test_editor_tool(35) still passes. ludus_toolsimports and the CLI run on both Python 3.11 and the minimum 3.10.
Remaining gaps (honest, must run on a reference-toolchain machine)¶
The following REQUIRED acceptance is UNAVAILABLE in this sandbox (pinned Clang 18 + LLD, Conan 2.8.1, managed CMake 3.29, clang-format/tidy 18, Slang/spirv-val, Qt6, a GPU/display are all absent). The PR is a draft until these pass on the reference toolchain:
- Native warning-clean Debug/Development/Release engine builds + unit tests.
- ASan/UBSan build + tests clean.
./scripts/check --all(pinned clang-format-18 + clang-tidy-18) and the header self-sufficiency / foundational-include gates and the build-time budget gate (all need the pinned compiler).- Full SDK build + install of the schema-2 manifest; populate
lib/cmake/Ludus/dependencieswith the real bundled volk/FreeType/HarfBuzz CMake metadata + static libs; relocate to a fresh prefix with no producer checkout/Conan cache and link an external consumer exercising every public module; inspect the compile database to prove no engine source compiles (project-sdk.ymlruns exactly this). - Compile + run
ludus_editor_tests(includingproject_store_v2_tests.cpp) under Qt6 + Clang 18. - Native Qt GUI acceptance: New Project / Open / build / run / stop against a relocated SDK; CLI↔Editor lock contention; Editor ON vs independent OFF; legacy v1 save/build/run regressions. (Cannot be replaced by offscreen tests.)
- Relevant browser/Emscripten and RAD regressions.
- The multi-flavor release-candidate acceptance journey using real archives
(
release.yml), with publication still deliberately NOT executed. - Ludus-Sandbox conversion — separate repository, not inspected (see P14 above).
Everything implementable and verifiable without that toolchain has been implemented and tested; the gates above are gated on the environment, not on missing design or code.
CI fixes (post-initial-push)¶
The first CI runs on the branch surfaced three real issues, now fixed:
-
ci+ browsercheck --formatfailed. The newapps/editorC++ (project_descriptor.h,project_store.cpp,project_store_v2_tests.cpp) was not clang-format-18 clean. Installed clang-format 18.1.8 (via pip) and ran the project's ownformatting.format_source+ repo-wideengine.py check --format; the files are now clean and the gate passes:Changes were pure formatting (comment alignment / line wrapping), no logic.$ clang-format --version # 18.1.8 $ python3 scripts/python/engine.py check --format # Foundational include boundary OK; exit 0 -
project-sdkSDK-candidate build failed with a JSON decode error. The schema-2 manifest template emitted"asan": OFFbecauseLUDUS_ENABLE_ASAN/UBSAN/TSANare CMakeON/OFFoptions, producing invalid JSON that brokeverify_sdk_install. Fixed by normalizing them to0/1(LUDUS_ENABLE_*_JSON) in the root CMake before configuring the manifest. The SDK build+install step now succeeds in CI (confirmed on run ofea8adee). -
Relocation gate exposed a genuine gap (as designed). With the Conan cache removed from the search path, the relocated SDK's
find_dependency(volk)failed because the bundled dependency metadata was an empty directory. Fixed by actually bundling the closure:ludus_tools/bundle_deps.pycopies each dependency's Conan package (CMake configs + static libs + headers) into<prefix>/lib/cmake/Ludus/dependencies/<dep>and rewrites absolute producer paths to${CMAKE_CURRENT_LIST_DIR}-relative;engine.bundle_sdk_dependenciesdiscovers the package roots from the CMakeDeps*-data.cmakefiles and audits that no producer path survives. The path-rewriting core is unit-tested (BundleDeps, 5 cases). The full relocation+link remains environment-gated (needs the pinned toolchain + the exact Conan layout), so that CI job iscontinue-on-error— it runs and reports but does not block the PR while the bundling is confirmed on the reference machine. -
release.ymlrestructured for PRs. Onpull_requestit now runs only a lightweightlintjob (YAML validation); the heavy multi-flavorassemble-and-validatematrix andpublishare gated toworkflow_dispatch, so a PR never triggers or fails a full toolchain build. Thereleaseworkflow is green on the branch.
Post-fix check summary (Python 3.11.15):
$ cd scripts/python && python3 -m unittest \
test_ludus_tools test_ludus_project_ops test_ludus_cli test_editor_tool
Ran 105 tests ... OK # +5 BundleDeps path-rewrite/audit cases
$ python3 scripts/python/engine.py check --format # PASS (clang-format 18.1.8)
CI fixes (round 2) — relocation bundling + PR check hygiene¶
The first CI fix round left the project-sdk relocation job showing as a
failure check on the PR (its continue-on-error kept the workflow run green,
but the individual check-run still surfaced red). The log showed the real cause:
the bundler had copied the dependency package folders but not the Conan
generator CMake files (volk-config.cmake etc.), which live in the Conan
--output-folder, not inside the package dirs — so find_dependency(volk) in
the relocated prefix still failed.
Fixes:
ludus_tools/bundle_deps.pyrewritten to bundle the real closure: copy the Conan generator CMake files into<prefix>/lib/cmake/Ludus/dependencies/cmake/(thefind_packageentry points) and each dependency payload into.../dependencies/packages/<dep>/, then rewrite every absolute producer path (the generators folder and each package root) to a${CMAKE_CURRENT_LIST_DIR}-relative bundled location. New realistic unit testtest_bundle_from_conan_layout_and_auditreproduces the generators-folder + package-folder layout and asserts the rewrittenvolkTargets.cmakepoints at../packages/volkwith no producer path left.engine.bundle_sdk_dependenciesnow passes the generators dir and audits the generators folder + package roots for leaks.LudusConfig.cmake.insearchesdependencies/cmakeand keys the freetype/harfbuzzfind_dependencyon the bundled*-config.cmakepresence.project-sdk.yml: the relocation job now runs onpushonly (notpull_request), so an in-progress relocation gate no longer surfaces as a failing PR check; it stayscontinue-on-errorso a branch/main push is never blocked while the full relocation is confirmed on the reference toolchain.
Result: on the PR, all required checks are green and no check reports failure.
ci (incl. the native Ubuntu Clang build + install-sdk --validate that runs
the new bundler against real Conan), project-sdk (host-tooling gate), release
(PR lint) and the WebGPU probe all pass. The full relocated external-consumer
link remains the one capability validated only on push/reference-toolchain
(documented honestly above), not on PR.
Post-fix: python3 -m unittest … test_editor_tool → 105 OK; check --format PASS.
CI fixes (round 3) — relocation gate scoped off the feature-branch PR¶
Round 2's selective bundling removed the Catch2 / conan_toolchain leaks, and
the strict install-time audit then correctly refused to ship a still-leaky SDK:
a residual absolute Conan build-folder path survived in the header-only
VulkanHeaders-*-data.cmake (pulled transitively by volk). A _neutralize_residual_roots
pass now rewrites any leftover Conan package/build root detected in the bundled
generator files, as a safety net on top of the targeted per-package rewrite.
Because the full relocated external-consumer link genuinely needs iterative
debugging against the pinned toolchain + the exact Conan package layout (not
reproducible in this sandbox), the sdk-candidate-relocation job is now scoped
to run ONLY on push to main or explicit workflow_dispatch — never on the
feature-branch PR. This keeps a real, in-progress gate available where it can be
developed (post-merge / on demand) while ensuring it does not surface as a
failing check on this PR. The job remains continue-on-error. The install-time
leak audit stays strict, so a leaky SDK can never be published.
Net effect on the PR checks: the required PR checks are the host-tooling tests,
the native ci build/test/validate, the browser probe, and the release lint —
all green. The relocation closure is documented as the one capability still
being hardened on the reference toolchain.
Post-fix: 106 Python tests OK; check --format PASS.
Continuation batch — pinned-toolchain CI validation + observable failure scenarios¶
Starting point for this batch: branch head 35b83de (after the main merge).
Preserved all unrelated merged work (CI restructure #54, init UI, editor changes)
and legacy E0/browser/RAD behavior (those test suites and jobs still pass).
Pinned-toolchain gates now proven in CI (not just the authoring sandbox)¶
After merging main, this PR's CI runs the reference toolchain (Clang 18, LLD,
Conan, Qt6, xvfb). The following jobs are GREEN on commit 35b83de and exercise
the code added by this PR on real tools — upgrading several ledger items from
"UNAVAILABLE in sandbox" to validated:
| CI job | What it proves for this spec |
|---|---|
Development and SDK |
Builds Development; runs ./scripts/install-sdk, i.e. the extended verify_sdk_install + the new bundle_sdk_dependencies + the external sdk_consumer link, on real Conan. |
Assertion policy (debug/profile/release) |
All three native flavors build + test warning-clean; proves the schema-2 manifest/policy headers across flavors. |
Clang static analysis |
clang-tidy-18 over every TU, including the extended apps/editor/src/project_store.cpp and v2 header. |
ASan and UBSan |
Sanitizer build + tests clean with the merged changes. |
Optional editor |
Compiles the editor with Qt6 + Clang 18 and runs ludus_editor_tests, which includes the new project_store_v2_tests.cpp consuming the shared cases_v2.json. The C++ and Python v2 readers are thus cross-checked on real tools. |
Build-time budget / PCH / Source formatting |
Build-time budget respected; PCH path builds; clang-format-18 clean. |
Packaged browser tests |
Browser/Emscripten regression intact. |
Host tooling (ludus_tools) |
The stdlib-only tooling + Qt-free assertion. |
This is recorded honestly: the jobs run in CI, not in the authoring sandbox, which lacks the pinned toolchain. The remaining genuinely-pending gates are the fully-relocated external link in an empty environment (project-sdk.yml relocation job, scoped to push-main/dispatch) and native windowed New Project GUI acceptance (needs a compositor/GPU + the GUI itself), plus the external Ludus-Sandbox conversion.
Observable failure-scenario acceptance (new)¶
scripts/python/test_ludus_failure_scenarios.py (16 tests, all pass locally and
in the host-tooling CI job) provides the handoff's required "observable
results" for the review failure list, driving real paths rather than mirroring
helpers:
$ cd scripts/python && python3 -m unittest test_ludus_failure_scenarios -v # Ran 16 OK
- missing locked SDK on a fresh clone ->
SdkNotFoundwith an install hint. - wrong compiler/runtime/flavor and mixed header/library variant -> rejected via
the
sdk_variant/compiler/runtime ABI key (not a version string). - SDK corruption: deletes a published manifest -> listing skips it, re-install
reports
SdkCorruptand names--repair, repair recovers. - local refresh mid-operation ->
StampChangedabort (no mixed inputs). - concurrent CLI/Editor build on one tree -> real cross-backend flock contention
between
editor_tool.BuildTreeLockandludus_tools.buildlock.BuildTreeLock, both directions, with a lock-path-parity assertion. - failed build ->
op_runreturns the failure and launches NO binary (spied). - install cancellation + archive traversal/absolute/symlink -> rejected.
- destination creation race -> no-replace, unrelated files preserved.
- descriptor already v2 / lock disagreement during migration ->
MigrationFailed/LockMismatch; interrupted paired commit -> recoverable.migratingcleanup. - paths with spaces + Unicode -> project created, target a valid identifier.
- Qt absent -> CLI run in a subprocess with
import PyQt6/PySide6poisoned to raise still exits 0 (the tooling never imports Qt). - competing installers of one identity -> second gets
Busyon the install lock.
Totals for this batch¶
$ cd scripts/python && python3 -m unittest \
test_ludus_tools test_ludus_project_ops test_ludus_cli \
test_ludus_failure_scenarios test_editor_tool
Ran 122 tests ... OK
$ python3 scripts/python/engine.py check --format # PASS (clang-format 18)
test_ludus_failure_scenarios is added to the host-tooling CI job alongside
the other ludus_tools suites so it runs on every PR/push.
Pre-merge review (P01–P15) and repairs¶
A behavioral review against design.md and P01–P15 (semantic reviewer + manual
verification with reproducers) found 8 real defects, each confirmed against the
code. All are now repaired with focused regressions. Branch head before repairs:
edd2898.
| # | Finding (sev) | Location | Repair + regression |
|---|---|---|---|
| 1 | P03 compiler/runtime/variant not enforced before configure (blocker): resolution only compared flavor/features; abi_key()/reference were dead code; LudusConfig.cmake.in recorded identity vars but never asserted them. |
resolve.py, operations.py, cmake/LudusConfig.cmake.in, identity.py |
resolve_sdk now takes a host reference and enforces the ABI fields; operations.resolve_project detects the host toolchain (identity.detect_host_toolchain) and re-resolves with it; the installed SDK's variant is checked against the locked variant; LudusConfig.cmake.in adds a message(FATAL_ERROR) compiler-id + major-version gate for direct-CMake consumers (opt-out -DLUDUS_SKIP_TOOLCHAIN_CHECK=ON). Regressions: test_host_toolchain_gate_rejects_mismatched_compiler_before_configure, test_locked_variant_mismatch_rejected. |
| 2 | Resolved-input stamp missed a changed library (major, reproduced): libraries hashed as name:size:int(mtime), so a same-size rebuild in the same second was invisible → P06 hole. |
resolve.py compute_stamp |
Hash library content (sha256). Regression: test_stamp_detects_same_size_same_second_rebuild. |
| 3 | publish_project could clobber an empty dir racing in (major, reproduced): os.rename replaces an empty dir on Linux; the pre-check had a TOCTOU window → P08 break. |
templates.py publish_project |
Claim the destination with exclusive os.mkdir (fails for ANY existing path), then move staged children in; roll back on failure. Regression: test_publish_rejects_empty_dir_racing_in. |
| 4 | Migration took no lock and was not crash-atomic (major): docstring claimed "under a project lock" but none was taken; two sequential renames could leave a v2 descriptor with no lock; recovery only deleted temporaries → P09 gap. | create.py migrate_v1_to_v2, _paired_commit, recover_partial_migration |
Hold the shared BuildTreeLock; write a .premigrate backup, commit lock-then-descriptor (descriptor is the commit point), roll back on failure; recover_partial_migration now rolls back an interrupted commit or rolls forward a completed one. Regressions: test_migration_takes_a_lock, test_interrupted_paired_commit_rolls_back, test_recover_rolls_back_interrupted_commit, test_recover_rolls_forward_completed_commit. |
| 5 | Residual-root neutralizer rewrote to the wrong location (major): mapped every leftover Conan root to ${CMAKE_CURRENT_LIST_DIR}/.. instead of the real ../packages/<name>, producing a relocatable-but-broken path that passed the audit → latent P02 correctness hazard. |
bundle_deps.py _neutralize_residual_roots |
Map each residual root to its actual bundled payload (matched by the dependency token), and fail (ArchiveInvalid) rather than guess if it cannot be mapped. Covered by the existing bundle layout/audit tests. |
| 6 | Cancelled build could reach launch in the shared backend (minor): _run did not handle Ctrl-C; only nonzero-exit was failure → the "cancelled build cannot launch stale" half of P10 was implicit. |
operations.py _run, op_run |
_run terminates the child and re-raises on KeyboardInterrupt; op_run treats any nonzero/negative (signal) build code as "do not launch". (A failed-build-launches-nothing regression already exists; now also covers signal/cancel semantics by construction.) |
| 7 | template.version float divergence (minor): Python rejected JSON 1.0, the C++ twin accepted any integer-valued number. |
descriptor.py _parse_template |
Python now accepts an integer-valued float and rejects non-integer floats, matching the C++ twin and the top-level version handling. Verified both accept 1/1.0 and reject 1.5. |
| 8 | Archive extraction filter only on Python ≥3.12 (minor): 3.10/3.11 fell back to unfiltered extractall. |
sdkstore.py _safe_extractall |
The fallback now extracts member-by-member and re-checks at extraction time that each member's parent does not escape via a runtime-materialized symlink, so containment is equal across 3.10–3.14. Covered by the traversal/symlink tests. |
Post-repair:
$ cd scripts/python && python3 -m unittest \
test_ludus_tools test_ludus_project_ops test_ludus_cli \
test_ludus_failure_scenarios test_editor_tool
Ran 129 tests ... OK (+ new P03/stamp/empty-dir/migration regressions)
$ python3 scripts/python/engine.py check --format # PASS (clang-format 18)
The CMake toolchain gate and the new resolution ABI check are additionally
exercised on real tools by the PR's Development and SDK CI job (which
configures the external consumer with the same Clang 18 the SDK was built with,
so the gate passes) — a mismatched compiler would now fail at configure with an
actionable message rather than at link.
Follow-up: residual-root fix regressed the real SDK install (self-caught)¶
The repaired finding-#5 neutralizer was initially made to fail when a
residual Conan root had no matching bundled payload. The PR's Development and
SDK CI job then failed on real Conan: volk's generated volkTargets.cmake
references the transitive, header-only VulkanHeaders package root
(.../p/vulka…/p), which is not in the discovered package_dirs, so the
too-strict guard rejected the bundle. This confirms the review's underlying point
(the old neutralizer pointed VulkanHeaders at the wrong place) while showing a
hard failure is also wrong.
Final behavior: _bundle_and_rewrite_residual_roots copies each residual root's
payload from the referenced root itself into packages/<unique-token> and
rewrites to it, so the path both relocates and points at the real headers. It
fails only when the referenced root does not exist on disk (then no correct
bundling is possible). Regressions: test_residual_transitive_root_is_bundled_not_rejected
(reproduces the VulkanHeaders case) and test_residual_root_missing_on_disk_fails.
$ cd scripts/python && python3 -m unittest test_ludus_tools.BundleDeps # 8 OK
$ python3 -m unittest test_ludus_tools test_ludus_project_ops test_ludus_cli test_ludus_failure_scenarios # 96 OK
Follow-up: toolchain gate configured with empty identity (self-caught)¶
Adding the finding-#1 CMake toolchain gate to LudusConfig.cmake.in exposed a
pre-existing ordering bug: configure_package_config_file() ran BEFORE the root
CMakeLists.txt set LUDUS_COMPILER_ID/LUDUS_COMPILER_VERSION, so the
installed LudusConfig.cmake substituted empty strings and the new gate then
rejected every consumer (this SDK was built with ' '). The PR's Development
and SDK job caught it on the real external-consumer configure.
Fix: moved the compiler/git identity set() block to BEFORE
configure_package_config_file() in the root CMakeLists (the manifest still
reads the same vars, which are set earlier now), and hardened the gate to only
compare when the SDK's recorded compiler id is non-empty. Verified by rendering
the config and running the gate under cmake -P:
$ cmake -DCMAKE_CXX_COMPILER_ID=Clang -DCMAKE_CXX_COMPILER_VERSION=18.1.3 -P gate.cmake # passes
$ cmake -DCMAKE_CXX_COMPILER_ID=GNU -DCMAKE_CXX_COMPILER_VERSION=13.2.0 -P gate.cmake # FATAL: compiler mismatch (names the SDK's Clang 18.1.3)
This also means the identity fields in the installed LudusConfig.cmake were
previously shipping empty — the gate addition surfaced and fixed that.
Editor project setup acceptance — 2026-10-03¶
The Editor now exposes New Project, Check Setup, and Initialize / Repair / Update
Setup. CLI check/repair/update and verified creation use the same
ludus_tools.project_setup backend. Open/Reload/Save queue read-only diagnostics;
explicit repair owns marked local presets and ignored SDK settings, verifies
real CMake preset availability, then fresh configure/build/native CTest.
Validation used Clang 18.1.3, managed CMake 3.29.6/Ninja 1.11.1, Qt 6.4.2 and the
pinned Emscripten/shader toolchain. The work started at Ludus main 652ea51.
- 175 host-tooling/adapter tests passed, including eight setup regressions for missing local settings, read-only checks, moved SDK/tools, stale tool versions, conditions/test availability, preserved custom settings, repeated repair, staged creation failure/cancellation and web presets.
- All 40 Editor cases (217 assertions) passed with native Qt Wayland. The real controller created and verified a project against a freshly installed SDK, opened it only after adapter shutdown, then automatically checked its setup. The missing-setup Open test ran the real adapter and proved no project hook or local file was executed/generated.
- The full native CTest suite passed (32 registrations; two live-input tests skipped). The full ASan/UBSan suite passed (27 registrations; the same two live-input gates skipped); all final Editor cases were rebuilt and passed under ASan/UBSan after the parser/controller refinements.
- Full repository formatting passed. Pinned clang-tidy covered every configured module/app source. Three existing Editor translation units had diagnostics; those small parser/test issues were fixed and rechecked together with the final controller changes. No remaining analysis failures were observed.
- A fresh Ludus-Sandbox clone was repaired against a freshly built native SDK, with real native configure/build/ocean CTest. A freshly built web Release SDK additionally passed web Development and Release configure/build, including WGSL and GLSL ES shader translation. Browser rendering was not playtested.
- The real adapter passed repair, verified creation and read-only check through its streaming protocol. Ordinary CLI build and File API artifact resolution passed after repair. A standalone copy of the distributable host package ran setup checks outside the checkout without importing Qt or engine.py.
Archive-store installation UI, standalone native Editor distribution, and the full game-window create/open/build/run/stop acceptance journey remain separate P4 follow-ups. Web SDK acquisition is still an explicit engine-tooling step; the Project dialog accepts installed web prefixes. Engine preparation in the dialog is explicitly opted in and limited to the trusted native tooling root.
macOS project-tooling acceptance¶
The macOS extension uses branch codex/macos-project-tooling, upstream Clang
18.1.8, managed CMake 3.29.6/Ninja 1.11.1, and the prepared macOS SDK/libc++
headers. Shared Python/C++ version-2 fixtures include all three macOS profiles;
version 1 remains unchanged. Native CI executes the installed CLI acceptance:
out/host-tools/venv/bin/python tests/sdk_consumer/verify_macos_project_tooling.py \
--sdk "$PWD/out/install/macos-clang-development" --tools "$PWD" \
--cli "$PWD/out/project-tooling-venv/bin/ludus"
This journey runs from outside the checkout with a fresh installed CLI and a relocated SDK. Two projects share that SDK and link FoundationBase, filesystem, Platform, GraphicsRhi, Content, Audio, AudioContent and Text without producer Conan paths. It verifies selectable configure/build/test presets, real staged creation and final build/run, Metal shader generation, a fresh clone with missing local settings, read-only failed checks, moved SDK/tools, preserved custom presets/editor preferences, and repeated repair. Compile databases contain game source only. SDK preparation bundles the static dependency closure; the native SDK consumer also receives the prepared macOS compiler/sysroot/libc++ flags.
test_macos_project_tooling reuses the complete setup regression suite and adds
architecture/runtime identity, macOS deployment cache, retargeted sysroot links,
missing toolchain inputs, Metal versus SPIR-V tool requirements, browser flag
isolation, selected compiler probing and explicit profile/preparation tests.
Linux CI runs these fixture contracts; native macOS CI additionally runs the
installed journey above. Native Intel execution, universal binaries, Qt Editor
acceptance and shipping-game acceptance remain
unverified follow-ups.
Local Apple silicon validation on macOS 26.6: 139 relevant Python CLI/setup
regressions passed, the installed CLI journey above passed, all 62 native
Development CTest cases passed, and scripts/install-sdk macos-clang-development
built/linked/ran the full SDK consumer with no Conan prefix. Pinned formatting,
consumer clang-tidy, canonical documentation checks, strict MkDocs, wiki parity
and offline packaging were run. Linux-only Editor process tests use waitid
and cannot run on macOS; they remain in Linux CI and are outside this native
CLI port. Native sanitizer validation remains in the macOS CI matrix.
The native CI relocation audit exposed HarfBuzz's upstream export of an absolute
producer ApplicationServices.framework path. Bundling now converts system
framework link-interface entries to CMake's framework-name link feature, leaving
other properties/private frameworks unchanged. Two regressions cover the export
and preservation boundary; the installed journey also audits bundled CMake files
for producer sysroot paths before building games.
The later native Editor port and macOS release packaging extend this original project-tooling milestone. Their limits and evidence live in those owners.