Skip to content

[M0-TOOL-01] Public API manifest: laige-api-scanner + checked-in laige-api.json + CI drift check - #10

Merged
offdev merged 1 commit into
masterfrom
m0-tool-01-api-manifest
Sep 12, 2026
Merged

offdev merged 1 commit into
masterfrom
m0-tool-01-api-manifest

Conversation

@offdev

@offdev offdev commented Sep 12, 2026

Copy link
Copy Markdown
Owner

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>/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 single-line signature, doxygen summary, @budget <text> annotation, and @experimental flag. Public-only access; namespace detail excluded; 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, #define in class bodies. Full contract: the header comment of tools/api/laige-api.cpp.
  • laige-api CMake target — canonical regeneration: cmake --build build --target laige-api rewrites the checked-in root laige-api.json (PRD §9.4, NFR-13.1). The checked-in manifest (376 symbols from 10 headers, all in laige-core — the other M0 modules have no include/ dirs yet) is committed in this PR.
  • --check mode — byte compare (exit 0) or a symbol-level diff (added/removed/changed keyed by name+header+line, max 20 shown, parsed with laige::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.
  • CI — new api-manifest job in ci-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 in the same change (CORE-006) — docs/README.md index (DOC-001, previously missing), docs/getting-started/building.md status bullet (the canonical command was already reserved there), roadmap M0-TOOL-01 marked done with the decision record.

Key design decisions:

  • One entry per public symbol: class-scope members under private:/protected: are excluded (struct=public, class=private defaults); everything inside namespace detail is excluded; a qualified member definition at namespace scope (Prng::seedState(...)) produces no entry — the in-class declaration is the canonical one (overloads stay distinguishable via line).
  • Doc association: consecutive // 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/@experimental tag lines fill their fields.
  • The = 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-scanner is an executable and laige-api the custom target because CMake forbids two targets with the same name.

Incidental fix (latent since M0-CORE-08): isValidUtf8 in src/laige-core/json.cpp is [[maybe_unused]] — it is referenced only from debug asserts (fromString/setString UTF-8 preconditions), which compile out under NDEBUG, 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)
  • same in build-asan, build-tsan, build-clang, build-shared, build-clang-shared, and a fresh build-clang-scratch — 22/22 each
  • cmake --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
  • stale-fixture --check — exit 1 with + added: laige::E::B / ~ changed: laige::S (summary)
  • block-comment fixture — exit 2; nonexistent root — exit 2
  • two consecutive --out regenerations — byte-identical

Untested / 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 their include/ directories (the module list already mirrors tools/laige-include-lint).

…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
offdev merged commit 937ac7c into master Sep 12, 2026
9 checks passed
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)'.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant