[M0-TOOL-01] Public API manifest: laige-api-scanner + checked-in laige-api.json + CI drift check - #10
Merged
Merged
Conversation
…e-api.json + CI drift check A single C++20 tool (tools/api/laige-api-scanner) scans the public headers (src/<module>/include for the PRD §10.1 modules) and emits the deterministic machine-readable manifest laige-api.json: every public symbol (class/struct/enum/function/method/constructor/destructor/ variable/alias/enumerator/macro) with fully qualified name, header, line, normalized signature, doxygen summary, @Budget annotation, and @experimental flag. Public-only access; namespace detail excluded; out-of-line member definitions add no entries. The scanner is a doxygen-comment-driven text walker, deliberately narrow: unsupported constructs (block comments, raw strings, lambdas at declaration level, function-pointer parameter types, operator(), extern "C" blocks, compound typedefs, #define in class bodies) fail loudly (CORE-008); the full contract is the header comment of laige-api.cpp. - laige-api CMake target: canonical regeneration of the checked-in root laige-api.json (cmake --build build --target laige-api); the checked-in manifest (376 symbols from 10 headers, all laige-core) is committed and stays CI-checked. - --check mode: byte compare (exit 0) or a symbol-level diff added/removed/changed (exit 1, max 20 shown) parsed with laige::parseJson; exit 2 on any error. - CI: api-manifest job in ci-pull.yml/ci.yml regenerates the manifest on every PR/merge and fails on drift; CTest (tests/api) covers the fixture tree (exact manifest bytes), fresh/stale checks, the unsupported-construct failure, the root error, and the real-tree drift check in every P0 job. - Docs in the same change (CORE-006): docs/README.md index (DOC-001), building.md status bullet + reserved command, roadmap M0-TOOL-01 marked done with the decision record. - Fix (latent, from M0-CORE-08): isValidUtf8 in json.cpp is [[maybe_unused]] — the UTF-8 assert preconditions compile out under NDEBUG, which broke the Release build with -Werror=unused-function (CORE-010). Verified: all 22 CTest entries pass in build (Release), build-asan, build-tsan, build-clang, build-shared, build-clang-shared, and a fresh build-clang-scratch; regeneration is byte-identical.
offdev
added a commit
that referenced
this pull request
Sep 12, 2026
…contract + CI job (#12) * [M0-TOOL-02] Determinism checker skeleton: laige-detcheck + scenario contract + CI job laige-detcheck (tools/detcheck): runs a named scenario in two build configurations and compares per-tick state hash streams (FR-11.5, AGENTS ARCH-010, TEST-004). The engine state-hash API arrives with M1-DET-03, so the tool works against the documented hash-line output contract. Scenario contract (normative text in the tool header, mirrored in docs/api/detcheck.md): one stdout line per tick `<tick> <hash>` — tick starts at 0, step 1, no padding or leading zeros; hash = exactly 16 lowercase hex digits (the algorithm is NOT part of the contract — lines compare byte-for-byte, so only run-to-run identity matters); trailing newline optional, trailing \r tolerated (Windows CRLF); stderr ignored; exit 0 on completion. Enforced strictly and bounded (> 65536 ticks or > 64 bytes/line is a loud exit-2, CORE-008 — a malformed scenario is never a silent mismatch). Modes: --scenario=synthetic|synthetic-perturbed [--ticks=N] [--seed=X]: the built-in 32-body fpx16_16 + Prng workload, run twice in-process (pure integer arithmetic — bit-exact across build, platform, ISA, and compiler, ADR 0002; bodies seeded on an 8x8 grid spanning the 64-unit box so both wrap branches are exercised; the synthetic-perturbed run-b adds 1 unit to body 3's x at tick 7 — the step's Verify fixture). --run-a=<bin> --run-b=<bin> [-- scenario-args...]: the real mode from M1-DET-04 on — two builds of the same scenario source compared; the `--` separator keeps scenario arguments unambiguous (strict surface, API-008). Report (stdout, stable and machine-greppable — LOG-001): detcheck scenario=<name> result=OK ticks=<n> run-a: <label-or-path> run-b: <label-or-path> detcheck scenario=<name> result=DIVERGED first_diff_tick=<t> run-a: <t> <hashA> (the diverging tick pair, or run-b: <t> <hashB> `stream ends: <n> ticks` when one run ends early) Exit 0 = match · 1 = divergence (loud) · 2 = usage / unknown scenario / scenario failure / contract violation. Scenario execution: fork/exec + pipe capture (POSIX), CreateProcessW + PeekNamedPipe (Windows) — no shell, bounded memory, and the scenario's stderr stays visible in the CI log. The build type is stamped into the report labels (AGENTS 12) via LAIGE_DETCHECK_BUILD_TYPE. Tests (tests/detcheck, `ctest -R detcheck`) — 9 entries: the synthetic self-check, the built-in perturbation fixture, identical and different cross-binary pairs against fixture scenario binaries (one source, five compiled variants: clean / perturbed / bad output / early exit / short stream), the scenario-failure path, and unknown scenario. Each test is a generated `cmake -P` check script asserting BOTH the exit code and the required output fragments (the tests/api pattern; CTest inverts PASS_REGULAR_EXPRESSION under WILL_FAIL, and a crash exits non-zero like a correct failure, so the content must be checked). CI: new `detcheck` job in ci-pull.yml and ci.yml (every PR and merge, no ci:* condition — a tooling check like include-lint and api-manifest, not an additional P0 OS build). It builds and runs `laige-detcheck --scenario=synthetic`; the real-scenario step (two build configurations of M1-SAMPLE-01's hello) is skipped until that sample lands — M1-DET-04 activates it and records the result per ARCH-010. Docs (CORE-006, same change): docs/api/detcheck.md (contract, CLI, report/exit codes, M0 hash scope, CI status), building.md (current status entry + the tool-note now live), docs/README.md, tools/README.md, root README.md layout line. Local verify: ctest 31/31, zero warnings — g++ 16.2.1 Debug static, g++ shared, clang++ 22.1.8 Debug, clang++ ASan+UBSan, clang++ TSan (TSAN_OPTIONS=halt_on_error=1); `./build/bin/laige-detcheck --scenario=synthetic` exits 0 and `--scenario=synthetic-perturbed` exits 1 at first_diff_tick=7; the Windows execution path is compile-verified by the CI MSVC job (not testable on Linux). * [M0-TOOL-02] Board + change log (incl. retro M0-TOOL-01 row) Progress board: M0 17 -> 19 done (the board had not been updated for M0-TOOL-01 either — 18 boxes were checked while it said 17). Change log: M0-TOOL-02 row (fe4460c) + retroactive M0-TOOL-01 row (937ac7c/PR #10, merged without updating the board or this log). * [M0-TOOL-02] Record CI observation: ci-pull run 34697638239 green (7/7 active jobs) ci-pull.yml run of 82c548d: linux-gcc, linux-clang, linux-asan+UBSan, linux-tsan + include-lint, api-manifest, detcheck all passed; Windows/macOS label-skipped (default Linux lane); the new detcheck job's real-scenario step correctly reported 'skipped: no real scenario yet (M1-SAMPLE-01)'.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scope (M0-TOOL-01, per
roadmap/M0-foundations.md):tools/api/laige-api-scanner— single C++20 tool that scans the public headers (src/<module>/includefor the PRD §10.1 modules) and emits the deterministic machine-readable manifestlaige-api.json: every public symbol (class/struct/enum/function/method/constructor/destructor/variable/alias/enumerator/macro) with fully qualified name, header, line, normalized single-line signature, doxygen summary,@budget <text>annotation, and@experimentalflag. Public-only access;namespace detailexcluded; out-of-line member definitions, forward declarations, friend decls, and statements add no entries. The scanner is deliberately narrow (doxygen-comment-driven text walker, not a C++ parser) and fails loudly on unsupported constructs (CORE-008): block comments, raw strings, lambdas at declaration level, function-pointer parameter types,operator(),extern "C"blocks, compound typedefs,#definein class bodies. Full contract: the header comment oftools/api/laige-api.cpp.laige-apiCMake target — canonical regeneration:cmake --build build --target laige-apirewrites the checked-in rootlaige-api.json(PRD §9.4, NFR-13.1). The checked-in manifest (376 symbols from 10 headers, all inlaige-core— the other M0 modules have noinclude/dirs yet) is committed in this PR.--checkmode — byte compare (exit 0) or a symbol-level diff (added/removed/changed keyed by name+header+line, max 20 shown, parsed withlaige::parseJson) on mismatch (exit 1); exit 2 on any error. Deterministic output (no timestamps, NFR-13.4) makes byte-identical regeneration the CI drift check.api-manifestjob inci-pull.yml/ci.yml: regenerates the manifest on every PR/merge and fails on drift, so adding a public symbol without regenerating fails CI. CTest (tests/api) runs the same check against the real tree in every P0 job (api-real-tree), plus fixture-tree tests: exact manifest bytes, fresh check, stale check with diff needles, unsupported-construct failure, root error.docs/README.mdindex (DOC-001, previously missing),docs/getting-started/building.mdstatus bullet (the canonical command was already reserved there), roadmapM0-TOOL-01marked done with the decision record.Key design decisions:
private:/protected:are excluded (struct=public, class=private defaults); everything insidenamespace detailis excluded; a qualified member definition at namespace scope (Prng::seedState(...)) produces no entry — the in-class declaration is the canonical one (overloads stay distinguishable vialine).//lines directly above the declaration; blank comment line = paragraph break; banner lines (pure-=~*runs and--- section title ---headers) finalize without discarding; a physical blank line / closing brace / non-transparent directive discards the pending block (last block above a declaration wins); trailing//comments after code are wrapped continuations, never doc. Summary = first non-empty paragraph.@budget/@experimentaltag lines fill their fields.=of comparison operators (<=,>=,<=>) and of operator names (operator=,operator+=) is never an initializer; array-type[after a template-id (ElementSlot<T>[]) is a type suffix, not a bound.laige-api-scanneris an executable andlaige-apithe custom target because CMake forbids two targets with the same name.Incidental fix (latent since M0-CORE-08):
isValidUtf8insrc/laige-core/json.cppis[[maybe_unused]]— it is referenced only from debug asserts (fromString/setStringUTF-8 preconditions), which compile out underNDEBUG, breaking the canonical Release build with-Werror=unused-function(CORE-010). The other build trees are Debug, which is why it went unnoticed.Verification (commands run, all passing):
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release+cmake --build build -j+ctest— 22/22 (Release, GCC)build-asan,build-tsan,build-clang,build-shared,build-clang-shared, and a freshbuild-clang-scratch— 22/22 eachcmake --build build --target laige-api— regenerates the checked-in manifest byte-identically (md5 stable)./build/bin/laige-api-scanner --root . --check laige-api.json— exit 0--check— exit 1 with+ added: laige::E::B/~ changed: laige::S (summary)--outregenerations — byte-identicalUntested / remaining: MSVC (verified on the Windows CI job per convention — this PR adds no MSVC-specific code; the tool is POSIX-only in its file access via
std::filesystem, like the existing tools). The 8 non-core modules contribute no headers yet; their manifests land with theirinclude/directories (the module list already mirrorstools/laige-include-lint).