Skip to content

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 from main).
  • Working tree at branch creation: clean (git status --short empty); 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 unittest suites 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 existing LUDUS_SDK_VARIANT compatibility key (assert policy version, dialogs, asan, ubsan, tsan),
  • sorted features, assertion policy block, package format, 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.in extended to schema 2: adds source revision, target triple/os/arch, compiler id/version, cxx_runtime_abi, distro_baseline, build type, sanitizer block, a nested assert_policy block, sorted features, components, a redistributable dependencies inventory (name/version/licenses/kind) and system_prerequisites. The legacy flat enable_asserts/break_on_check/assert_policy_version/ assert_dialogs_available/build_flavor_id/sdk_variant fields are retained so engine.verify_sdk_install and cmake/CheckSdkVariant.cmake.in keep 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 by ludus_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 by Ludus::Text but 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 installs THIRD_PARTY_NOTICES.md and a stable package-local dependency search directory lib/cmake/Ludus/dependencies.
  • cmake/LudusConfig.cmake.in: prepends the bundled dependency directory to CMAKE_PREFIX_PATH locally (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 as Ludus_* vars, and adds a public ludus_apply_app_policy() helper (C++23 + no-exceptions) for templates so they never reference the checkout-only ludus_apply_project_defaults.
  • cmake/sdk/THIRD_PARTY_NOTICES.md (new): records bundled vs system dependencies and their obligations; the manifest dependencies array is the machine-readable twin.
  • scripts/python/engine.py verify_sdk_install: now also requires the notices file, audits ALL installed lib/cmake/Ludus/**.cmake for 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 bundled minimal native 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_project ties descriptor + unresolved lock + rendered template; --sdk records a local prefix ONLY in ignored .ludus/local.json and keeps the committed lock unresolved (no fabricated hashes). migrate_v1_to_v2 is explicit, preserves relative paths/launch settings, requires an engine selection, refuses provider ludus, and does a recoverable paired descriptor+lock commit (.migrating temporaries + 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 as editor_tool.BuildTreeLock so CLI and Editor interlock (second = Busy).
  • operations.py: synchronous configure/build/run with exact argv/no-shell, CMake File API artifact resolution via the existing cmake_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: the ludus sdk … / ludus project … commands with stable exit codes and --json. pyproject.toml packages a redistributable wheel exposing a ludus entry 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: added Version, EngineRequirement, TemplateRef, the HasEngine/HasTemplate flags and the version-2 limit constants (kept equal to ludus_tools.descriptor).
  • apps/editor/src/project_store.cpp: ParseDescriptor now 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 the linux-clang-release preset, the required-for-cmake / forbidden-for-ludus engine object (exact non-floating version, bounded unique components/ features) and the optional template object. SerializeDescriptor is now version-aware and round-trips the v2 fields.
  • New shared v2 fixtures apps/editor/tests/fixtures/cases_v2.json (+ the six valid_v2_*/invalid_v2_* files) consumed by BOTH readers.
  • apps/editor/tests/project_store_v2_tests.cpp runs the C++ reader against cases_v2.json and a v2 serialize round-trip; wired into ludus_editor_tests.
  • scripts/python/test_ludus_tools.py runs the Python v2 reader against the same cases_v2.json. The legacy v1-only editor_project.py and its test_editor_tool.py fixture 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 strict SdkStore.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, no out/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; a publish job gated to workflow_dispatch only, environment: release, least-privilege contents: 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.yml jobs 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 on ubuntu-24.04 with 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.py are unchanged (git diff --stat main empty), so v1 descriptor parsing and the E0 streaming protocol/supervision are preserved. test_editor_tool (35) still passes.
  • ludus_tools imports 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:

  1. Native warning-clean Debug/Development/Release engine builds + unit tests.
  2. ASan/UBSan build + tests clean.
  3. ./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).
  4. Full SDK build + install of the schema-2 manifest; populate lib/cmake/Ludus/dependencies with 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.yml runs exactly this).
  5. Compile + run ludus_editor_tests (including project_store_v2_tests.cpp) under Qt6 + Clang 18.
  6. 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.)
  7. Relevant browser/Emscripten and RAD regressions.
  8. The multi-flavor release-candidate acceptance journey using real archives (release.yml), with publication still deliberately NOT executed.
  9. 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:

  1. ci + browser check --format failed. The new apps/editor C++ (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 own formatting.format_source + repo-wide engine.py check --format; the files are now clean and the gate passes:

    $ clang-format --version   # 18.1.8
    $ python3 scripts/python/engine.py check --format   # Foundational include boundary OK; exit 0
    
    Changes were pure formatting (comment alignment / line wrapping), no logic.

  2. project-sdk SDK-candidate build failed with a JSON decode error. The schema-2 manifest template emitted "asan": OFF because LUDUS_ENABLE_ASAN/UBSAN/TSAN are CMake ON/OFF options, producing invalid JSON that broke verify_sdk_install. Fixed by normalizing them to 0/1 (LUDUS_ENABLE_*_JSON) in the root CMake before configuring the manifest. The SDK build+install step now succeeds in CI (confirmed on run of ea8adee).

  3. 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.py copies 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_dependencies discovers the package roots from the CMakeDeps *-data.cmake files 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 is continue-on-error — it runs and reports but does not block the PR while the bundling is confirmed on the reference machine.

  4. release.yml restructured for PRs. On pull_request it now runs only a lightweight lint job (YAML validation); the heavy multi-flavor assemble-and-validate matrix and publish are gated to workflow_dispatch, so a PR never triggers or fails a full toolchain build. The release workflow 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.py rewritten to bundle the real closure: copy the Conan generator CMake files into <prefix>/lib/cmake/Ludus/dependencies/cmake/ (the find_package entry 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 test test_bundle_from_conan_layout_and_audit reproduces the generators-folder + package-folder layout and asserts the rewritten volkTargets.cmake points at ../packages/volk with no producer path left.
  • engine.bundle_sdk_dependencies now passes the generators dir and audits the generators folder + package roots for leaks.
  • LudusConfig.cmake.in searches dependencies/cmake and keys the freetype/harfbuzz find_dependency on the bundled *-config.cmake presence.
  • project-sdk.yml: the relocation job now runs on push only (not pull_request), so an in-progress relocation gate no longer surfaces as a failing PR check; it stays continue-on-error so 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 -> SdkNotFound with 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 SdkCorrupt and names --repair, repair recovers.
  • local refresh mid-operation -> StampChanged abort (no mixed inputs).
  • concurrent CLI/Editor build on one tree -> real cross-backend flock contention between editor_tool.BuildTreeLock and ludus_tools.buildlock.BuildTreeLock, both directions, with a lock-path-parity assertion.
  • failed build -> op_run returns 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 .migrating cleanup.
  • paths with spaces + Unicode -> project created, target a valid identifier.
  • Qt absent -> CLI run in a subprocess with import PyQt6/PySide6 poisoned to raise still exits 0 (the tooling never imports Qt).
  • competing installers of one identity -> second gets Busy on 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.