From fe0f5a5b28193239eee51b64cbc24712c0ce5e24 Mon Sep 17 00:00:00 2001 From: Pascal Severin Date: Sat, 12 Sep 2026 13:36:17 +0200 Subject: [PATCH 1/6] [M0-CORE-08] Fix Windows CI: missing from budget_harness.h (MSVC C2039/C3861 on std::sort) Histogram::stats() calls std::copy_n and std::sort in budget_harness.h, but the header included only ///// /. GCC and Clang pull in transitively (via and friends), so the linux lanes and local builds pass, but MSVC standard headers do not: the windows-msvc job died with C2039 (sort: is not a member of std) / C3861 while compiling budget_harness.cpp. - Add #include to the header (self-contained per CPP-010; alphabetically first in the include block). - Regenerate the checked-in laige-api.json with laige-api-scanner: the manifest records per-symbol line numbers, and the added include shifts every budget_harness.h entry by one line (58 lines; signatures and summaries unchanged). Without it the api-real-tree drift check fails in every P0 job, including windows-msvc, right after the build fix. Verified: full build + all 22 CTest entries pass in build; include-lint passes (17 source files scanned, 78 system headers, 1 vendored dependency); audited every CI-built TU (src/tests/tools) for std symbols used without a direct include - no other gaps. --- laige-api.json | 116 +++++++++--------- src/laige-core/include/laige/budget_harness.h | 1 + 2 files changed, 59 insertions(+), 58 deletions(-) diff --git a/laige-api.json b/laige-api.json index f04d23c..dc41f8e 100644 --- a/laige-api.json +++ b/laige-api.json @@ -14,64 +14,64 @@ "src/laige-core/include/laige/sim_math.h" ], "symbols": [ - {"name": "laige::HistogramStats", "kind": "struct", "header": "src/laige-core/include/laige/budget_harness.h", "line": 154, "signature": "struct HistogramStats", "summary": "Summary statistics over the samples currently stored in a Histogram (rolling window). When n == 0 the six statistics are NaN (check n; budgetCheck turns an empty histogram into a loud NO_SAMPLES failure).", "budget": null, "experimental": false}, - {"name": "laige::HistogramStats::n", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 155, "signature": "std::uint64_t n", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::HistogramStats::min", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 156, "signature": "double min", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::HistogramStats::mean", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 157, "signature": "double mean", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::HistogramStats::p50", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 158, "signature": "double p50", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::HistogramStats::p95", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 159, "signature": "double p95", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::HistogramStats::p99", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 160, "signature": "double p99", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::HistogramStats::max", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 161, "signature": "double max", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::Histogram", "kind": "class", "header": "src/laige-core/include/laige/budget_harness.h", "line": 167, "signature": "class Histogram", "summary": "A fixed-capacity, allocation-free sample store (rolling window). See the header preamble for the full contract (window semantics, nearest-rank percentile definition, performance, threading).", "budget": null, "experimental": false}, - {"name": "laige::Histogram::Options", "kind": "struct", "header": "src/laige-core/include/laige/budget_harness.h", "line": 169, "signature": "struct Options", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::Histogram::Options::capacity", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 173, "signature": "std::size_t capacity = 0", "summary": "Capacity: the maximum number of samples kept. 0 is legal: every record() is dropped and stats() is always empty (useful as a churn-only counter, and as the loud-failure state for budgetCheck).", "budget": null, "experimental": false}, - {"name": "laige::Histogram::Histogram", "kind": "constructor", "header": "src/laige-core/include/laige/budget_harness.h", "line": 178, "signature": "explicit Histogram(Options options) noexcept : capacity_(options.capacity), window_(options.capacity), scratch_(options.capacity), cursor_(0), count_(0), total_(0)", "summary": "Setup path: performs the two backing allocations (window + scratch sort buffer). O(capacity) time and space.", "budget": null, "experimental": false}, - {"name": "laige::Histogram::Histogram", "kind": "constructor", "header": "src/laige-core/include/laige/budget_harness.h", "line": 187, "signature": "Histogram(const Histogram&) = default", "summary": "Value semantics: copy is O(capacity) (deep, cold path), move O(1).", "budget": null, "experimental": false}, - {"name": "laige::Histogram::Histogram", "kind": "constructor", "header": "src/laige-core/include/laige/budget_harness.h", "line": 188, "signature": "Histogram(Histogram&&) = default", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::Histogram::operator=", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 189, "signature": "Histogram& operator=(const Histogram&) = default", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::Histogram::operator=", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 190, "signature": "Histogram& operator=(Histogram&&) = default", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::Histogram::record", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 196, "signature": "void record(double value) noexcept", "summary": "Record one sample (unit: whatever the caller measures — typically milliseconds from a TimeIt). O(1), no allocation, no lock, noexcept. When the window is full the oldest sample is dropped; totalRecorded() keeps counting, so truncation is observable.", "budget": null, "experimental": false}, - {"name": "laige::Histogram::reset", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 206, "signature": "void reset() noexcept", "summary": "Drop every stored sample (count -> 0). totalRecorded() survives (since-construction churn; per-frame profilers diff it). Idempotent.", "budget": null, "experimental": false}, - {"name": "laige::Histogram::count", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 212, "signature": "[[nodiscard]] std::uint64_t count() const noexcept", "summary": "Samples currently stored in the window (<= capacity).", "budget": null, "experimental": false}, - {"name": "laige::Histogram::totalRecorded", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 215, "signature": "[[nodiscard]] std::uint64_t totalRecorded() const noexcept", "summary": "Samples recorded since construction, including dropped ones (churn).", "budget": null, "experimental": false}, - {"name": "laige::Histogram::stats", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 221, "signature": "[[nodiscard]] HistogramStats stats() const", "summary": "Exact statistics over the stored window (nearest-rank percentiles — see the preamble). Cold path: O(n log n) time, no allocation (sorts the pre-allocated scratch buffer; logically const — scratch_ is a reusable work buffer, not state).", "budget": null, "experimental": false}, - {"name": "laige::TimeIt", "kind": "class", "header": "src/laige-core/include/laige/budget_harness.h", "line": 281, "signature": "class TimeIt", "summary": "A scope timer over std::chrono::steady_clock (monotonic — see the preamble). Milliseconds as a double. No allocation, no lock.", "budget": null, "experimental": false}, - {"name": "laige::TimeIt::TimeIt", "kind": "constructor", "header": "src/laige-core/include/laige/budget_harness.h", "line": 284, "signature": "TimeIt() noexcept : start_(std::chrono::steady_clock::now())", "summary": "Starts the scope now.", "budget": null, "experimental": false}, - {"name": "laige::TimeIt::reset", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 287, "signature": "void reset() noexcept", "summary": "Restarts the scope (owner thread only).", "budget": null, "experimental": false}, - {"name": "laige::TimeIt::elapsedMs", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 290, "signature": "[[nodiscard]] double elapsedMs() const noexcept", "summary": "Elapsed time in milliseconds since construction/reset.", "budget": null, "experimental": false}, - {"name": "laige::BudgetMetric", "kind": "enum", "header": "src/laige-core/include/laige/budget_harness.h", "line": 306, "signature": "enum class BudgetMetric : std::uint8_t", "summary": "The statistic of a Histogram that a budget entry checks.", "budget": null, "experimental": false}, - {"name": "laige::BudgetMetric::Mean", "kind": "enumerator", "header": "src/laige-core/include/laige/budget_harness.h", "line": 306, "signature": "Mean", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetMetric::Min", "kind": "enumerator", "header": "src/laige-core/include/laige/budget_harness.h", "line": 306, "signature": "Min", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetMetric::Max", "kind": "enumerator", "header": "src/laige-core/include/laige/budget_harness.h", "line": 306, "signature": "Max", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetMetric::P50", "kind": "enumerator", "header": "src/laige-core/include/laige/budget_harness.h", "line": 306, "signature": "P50", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetMetric::P95", "kind": "enumerator", "header": "src/laige-core/include/laige/budget_harness.h", "line": 306, "signature": "P95", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetMetric::P99", "kind": "enumerator", "header": "src/laige-core/include/laige/budget_harness.h", "line": 306, "signature": "P99", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetEntry", "kind": "struct", "header": "src/laige-core/include/laige/budget_harness.h", "line": 310, "signature": "struct BudgetEntry", "summary": "One named budget from budgets.json (PRD 8.1 row -> entry). See the preamble for the target/measured/unit/metric contract.", "budget": null, "experimental": false}, - {"name": "laige::BudgetEntry::name", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 311, "signature": "std::string name", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetEntry::metric", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 312, "signature": "BudgetMetric metric", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetEntry::unit", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 313, "signature": "std::string unit", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetEntry::target", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 314, "signature": "double target", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetEntry::measured", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 315, "signature": "double measured", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetEntry::workload", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 316, "signature": "std::string workload", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetTable", "kind": "class", "header": "src/laige-core/include/laige/budget_harness.h", "line": 322, "signature": "class BudgetTable", "summary": "The parsed budgets.json (schema v1). Immutable after loading; safe to read from any thread. Small by design (one row per PRD 8.1 budget): find() is a linear scan on the cold path (no hash map, PERF-006).", "budget": null, "experimental": false}, - {"name": "laige::BudgetTable::BudgetTable", "kind": "constructor", "header": "src/laige-core/include/laige/budget_harness.h", "line": 324, "signature": "BudgetTable() = default", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetTable::size", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 326, "signature": "[[nodiscard]] std::size_t size() const noexcept", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetTable::entries", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 329, "signature": "[[nodiscard]] const std::vector& entries() const noexcept", "summary": "The entries in file order.", "budget": null, "experimental": false}, - {"name": "laige::BudgetTable::find", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 334, "signature": "[[nodiscard]] const BudgetEntry* find(std::string_view name) const noexcept", "summary": "The entry with the given name, or nullptr when absent.", "budget": null, "experimental": false}, - {"name": "laige::BudgetReportContext", "kind": "struct", "header": "src/laige-core/include/laige/budget_harness.h", "line": 349, "signature": "struct BudgetReportContext", "summary": "Caller-supplied context for the AGENTS 12 report fields the harness cannot know (machine/build facts). The caller (benchmark runner / operator) fills these; budgetCheck formats them verbatim into the report (diagnostic text, not engine state). Defaults to all-empty.", "budget": null, "experimental": false}, - {"name": "laige::BudgetReportContext::workload", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 350, "signature": "const char* workload = \"\"", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetReportContext::build", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 351, "signature": "const char* build = \"\"", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetReportContext::machine", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 352, "signature": "const char* machine = \"\"", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetReportContext::warmup", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 353, "signature": "std::uint32_t warmup = 0", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::formatStatsLine", "kind": "function", "header": "src/laige-core/include/laige/budget_harness.h", "line": 362, "signature": "[[nodiscard]] std::string formatStatsLine(const HistogramStats& s)", "summary": "The stable one-line text form of a HistogramStats value: `stats: n= min= mean= p50= p95= p99= max=` (6 significant digits, locale-free; \"nan\" for an empty histogram). The report lines of budgetCheck and the laige-bench tool both use this, so the stats text has one source (LOG-001 stable fields). Cold path: allocates one string.", "budget": null, "experimental": false}, - {"name": "laige::BudgetCheckResult", "kind": "struct", "header": "src/laige-core/include/laige/budget_harness.h", "line": 366, "signature": "struct BudgetCheckResult", "summary": "The outcome of a budget check plus the formatted AGENTS 12 report (stable multi-line text; format in docs/api/budget_harness.md).", "budget": null, "experimental": false}, - {"name": "laige::BudgetCheckResult::passed", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 367, "signature": "bool passed", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetCheckResult::measured", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 368, "signature": "double measured", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetCheckResult::target", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 370, "signature": "double target", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetCheckResult::before", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 371, "signature": "double before", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::BudgetCheckResult::report", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 372, "signature": "std::string report", "summary": null, "budget": null, "experimental": false}, - {"name": "laige::loadBudgets", "kind": "function", "header": "src/laige-core/include/laige/budget_harness.h", "line": 385, "signature": "[[nodiscard]] Result loadBudgets(std::string_view path)", "summary": "Loads and validates budgets.json (schema v1) from `path`.", "budget": null, "experimental": false}, - {"name": "laige::budgetCheck", "kind": "function", "header": "src/laige-core/include/laige/budget_harness.h", "line": 394, "signature": "[[nodiscard]] BudgetCheckResult budgetCheck(const BudgetEntry& entry, const Histogram& histogram, const BudgetReportContext& context = {})", "summary": "Checks `entry` against `histogram` (semantics in the preamble: NO_SAMPLES / target>0 at-most / target==0 hard zero). before/after are entry.measured / the current measurement (the AGENTS 12 before/after pair). Cold path: O(n log n) (the stats pass) plus report string building (allocates — reporting is never a hot path). Thread-safe on const inputs; the histogram must not be mutated concurrently (CONC-001 single-owner rule).", "budget": null, "experimental": false}, + {"name": "laige::HistogramStats", "kind": "struct", "header": "src/laige-core/include/laige/budget_harness.h", "line": 155, "signature": "struct HistogramStats", "summary": "Summary statistics over the samples currently stored in a Histogram (rolling window). When n == 0 the six statistics are NaN (check n; budgetCheck turns an empty histogram into a loud NO_SAMPLES failure).", "budget": null, "experimental": false}, + {"name": "laige::HistogramStats::n", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 156, "signature": "std::uint64_t n", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::HistogramStats::min", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 157, "signature": "double min", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::HistogramStats::mean", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 158, "signature": "double mean", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::HistogramStats::p50", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 159, "signature": "double p50", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::HistogramStats::p95", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 160, "signature": "double p95", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::HistogramStats::p99", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 161, "signature": "double p99", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::HistogramStats::max", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 162, "signature": "double max", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::Histogram", "kind": "class", "header": "src/laige-core/include/laige/budget_harness.h", "line": 168, "signature": "class Histogram", "summary": "A fixed-capacity, allocation-free sample store (rolling window). See the header preamble for the full contract (window semantics, nearest-rank percentile definition, performance, threading).", "budget": null, "experimental": false}, + {"name": "laige::Histogram::Options", "kind": "struct", "header": "src/laige-core/include/laige/budget_harness.h", "line": 170, "signature": "struct Options", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::Histogram::Options::capacity", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 174, "signature": "std::size_t capacity = 0", "summary": "Capacity: the maximum number of samples kept. 0 is legal: every record() is dropped and stats() is always empty (useful as a churn-only counter, and as the loud-failure state for budgetCheck).", "budget": null, "experimental": false}, + {"name": "laige::Histogram::Histogram", "kind": "constructor", "header": "src/laige-core/include/laige/budget_harness.h", "line": 179, "signature": "explicit Histogram(Options options) noexcept : capacity_(options.capacity), window_(options.capacity), scratch_(options.capacity), cursor_(0), count_(0), total_(0)", "summary": "Setup path: performs the two backing allocations (window + scratch sort buffer). O(capacity) time and space.", "budget": null, "experimental": false}, + {"name": "laige::Histogram::Histogram", "kind": "constructor", "header": "src/laige-core/include/laige/budget_harness.h", "line": 188, "signature": "Histogram(const Histogram&) = default", "summary": "Value semantics: copy is O(capacity) (deep, cold path), move O(1).", "budget": null, "experimental": false}, + {"name": "laige::Histogram::Histogram", "kind": "constructor", "header": "src/laige-core/include/laige/budget_harness.h", "line": 189, "signature": "Histogram(Histogram&&) = default", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::Histogram::operator=", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 190, "signature": "Histogram& operator=(const Histogram&) = default", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::Histogram::operator=", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 191, "signature": "Histogram& operator=(Histogram&&) = default", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::Histogram::record", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 197, "signature": "void record(double value) noexcept", "summary": "Record one sample (unit: whatever the caller measures — typically milliseconds from a TimeIt). O(1), no allocation, no lock, noexcept. When the window is full the oldest sample is dropped; totalRecorded() keeps counting, so truncation is observable.", "budget": null, "experimental": false}, + {"name": "laige::Histogram::reset", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 207, "signature": "void reset() noexcept", "summary": "Drop every stored sample (count -> 0). totalRecorded() survives (since-construction churn; per-frame profilers diff it). Idempotent.", "budget": null, "experimental": false}, + {"name": "laige::Histogram::count", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 213, "signature": "[[nodiscard]] std::uint64_t count() const noexcept", "summary": "Samples currently stored in the window (<= capacity).", "budget": null, "experimental": false}, + {"name": "laige::Histogram::totalRecorded", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 216, "signature": "[[nodiscard]] std::uint64_t totalRecorded() const noexcept", "summary": "Samples recorded since construction, including dropped ones (churn).", "budget": null, "experimental": false}, + {"name": "laige::Histogram::stats", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 222, "signature": "[[nodiscard]] HistogramStats stats() const", "summary": "Exact statistics over the stored window (nearest-rank percentiles — see the preamble). Cold path: O(n log n) time, no allocation (sorts the pre-allocated scratch buffer; logically const — scratch_ is a reusable work buffer, not state).", "budget": null, "experimental": false}, + {"name": "laige::TimeIt", "kind": "class", "header": "src/laige-core/include/laige/budget_harness.h", "line": 282, "signature": "class TimeIt", "summary": "A scope timer over std::chrono::steady_clock (monotonic — see the preamble). Milliseconds as a double. No allocation, no lock.", "budget": null, "experimental": false}, + {"name": "laige::TimeIt::TimeIt", "kind": "constructor", "header": "src/laige-core/include/laige/budget_harness.h", "line": 285, "signature": "TimeIt() noexcept : start_(std::chrono::steady_clock::now())", "summary": "Starts the scope now.", "budget": null, "experimental": false}, + {"name": "laige::TimeIt::reset", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 288, "signature": "void reset() noexcept", "summary": "Restarts the scope (owner thread only).", "budget": null, "experimental": false}, + {"name": "laige::TimeIt::elapsedMs", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 291, "signature": "[[nodiscard]] double elapsedMs() const noexcept", "summary": "Elapsed time in milliseconds since construction/reset.", "budget": null, "experimental": false}, + {"name": "laige::BudgetMetric", "kind": "enum", "header": "src/laige-core/include/laige/budget_harness.h", "line": 307, "signature": "enum class BudgetMetric : std::uint8_t", "summary": "The statistic of a Histogram that a budget entry checks.", "budget": null, "experimental": false}, + {"name": "laige::BudgetMetric::Mean", "kind": "enumerator", "header": "src/laige-core/include/laige/budget_harness.h", "line": 307, "signature": "Mean", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetMetric::Min", "kind": "enumerator", "header": "src/laige-core/include/laige/budget_harness.h", "line": 307, "signature": "Min", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetMetric::Max", "kind": "enumerator", "header": "src/laige-core/include/laige/budget_harness.h", "line": 307, "signature": "Max", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetMetric::P50", "kind": "enumerator", "header": "src/laige-core/include/laige/budget_harness.h", "line": 307, "signature": "P50", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetMetric::P95", "kind": "enumerator", "header": "src/laige-core/include/laige/budget_harness.h", "line": 307, "signature": "P95", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetMetric::P99", "kind": "enumerator", "header": "src/laige-core/include/laige/budget_harness.h", "line": 307, "signature": "P99", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetEntry", "kind": "struct", "header": "src/laige-core/include/laige/budget_harness.h", "line": 311, "signature": "struct BudgetEntry", "summary": "One named budget from budgets.json (PRD 8.1 row -> entry). See the preamble for the target/measured/unit/metric contract.", "budget": null, "experimental": false}, + {"name": "laige::BudgetEntry::name", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 312, "signature": "std::string name", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetEntry::metric", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 313, "signature": "BudgetMetric metric", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetEntry::unit", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 314, "signature": "std::string unit", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetEntry::target", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 315, "signature": "double target", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetEntry::measured", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 316, "signature": "double measured", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetEntry::workload", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 317, "signature": "std::string workload", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetTable", "kind": "class", "header": "src/laige-core/include/laige/budget_harness.h", "line": 323, "signature": "class BudgetTable", "summary": "The parsed budgets.json (schema v1). Immutable after loading; safe to read from any thread. Small by design (one row per PRD 8.1 budget): find() is a linear scan on the cold path (no hash map, PERF-006).", "budget": null, "experimental": false}, + {"name": "laige::BudgetTable::BudgetTable", "kind": "constructor", "header": "src/laige-core/include/laige/budget_harness.h", "line": 325, "signature": "BudgetTable() = default", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetTable::size", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 327, "signature": "[[nodiscard]] std::size_t size() const noexcept", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetTable::entries", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 330, "signature": "[[nodiscard]] const std::vector& entries() const noexcept", "summary": "The entries in file order.", "budget": null, "experimental": false}, + {"name": "laige::BudgetTable::find", "kind": "method", "header": "src/laige-core/include/laige/budget_harness.h", "line": 335, "signature": "[[nodiscard]] const BudgetEntry* find(std::string_view name) const noexcept", "summary": "The entry with the given name, or nullptr when absent.", "budget": null, "experimental": false}, + {"name": "laige::BudgetReportContext", "kind": "struct", "header": "src/laige-core/include/laige/budget_harness.h", "line": 350, "signature": "struct BudgetReportContext", "summary": "Caller-supplied context for the AGENTS 12 report fields the harness cannot know (machine/build facts). The caller (benchmark runner / operator) fills these; budgetCheck formats them verbatim into the report (diagnostic text, not engine state). Defaults to all-empty.", "budget": null, "experimental": false}, + {"name": "laige::BudgetReportContext::workload", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 351, "signature": "const char* workload = \"\"", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetReportContext::build", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 352, "signature": "const char* build = \"\"", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetReportContext::machine", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 353, "signature": "const char* machine = \"\"", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetReportContext::warmup", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 354, "signature": "std::uint32_t warmup = 0", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::formatStatsLine", "kind": "function", "header": "src/laige-core/include/laige/budget_harness.h", "line": 363, "signature": "[[nodiscard]] std::string formatStatsLine(const HistogramStats& s)", "summary": "The stable one-line text form of a HistogramStats value: `stats: n= min= mean= p50= p95= p99= max=` (6 significant digits, locale-free; \"nan\" for an empty histogram). The report lines of budgetCheck and the laige-bench tool both use this, so the stats text has one source (LOG-001 stable fields). Cold path: allocates one string.", "budget": null, "experimental": false}, + {"name": "laige::BudgetCheckResult", "kind": "struct", "header": "src/laige-core/include/laige/budget_harness.h", "line": 367, "signature": "struct BudgetCheckResult", "summary": "The outcome of a budget check plus the formatted AGENTS 12 report (stable multi-line text; format in docs/api/budget_harness.md).", "budget": null, "experimental": false}, + {"name": "laige::BudgetCheckResult::passed", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 368, "signature": "bool passed", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetCheckResult::measured", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 369, "signature": "double measured", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetCheckResult::target", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 371, "signature": "double target", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetCheckResult::before", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 372, "signature": "double before", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::BudgetCheckResult::report", "kind": "variable", "header": "src/laige-core/include/laige/budget_harness.h", "line": 373, "signature": "std::string report", "summary": null, "budget": null, "experimental": false}, + {"name": "laige::loadBudgets", "kind": "function", "header": "src/laige-core/include/laige/budget_harness.h", "line": 386, "signature": "[[nodiscard]] Result loadBudgets(std::string_view path)", "summary": "Loads and validates budgets.json (schema v1) from `path`.", "budget": null, "experimental": false}, + {"name": "laige::budgetCheck", "kind": "function", "header": "src/laige-core/include/laige/budget_harness.h", "line": 395, "signature": "[[nodiscard]] BudgetCheckResult budgetCheck(const BudgetEntry& entry, const Histogram& histogram, const BudgetReportContext& context = {})", "summary": "Checks `entry` against `histogram` (semantics in the preamble: NO_SAMPLES / target>0 at-most / target==0 hard zero). before/after are entry.measured / the current measurement (the AGENTS 12 before/after pair). Cold path: O(n log n) (the stats pass) plus report string building (allocates — reporting is never a hot path). Thread-safe on const inputs; the histogram must not be mutated concurrently (CONC-001 single-owner rule).", "budget": null, "experimental": false}, {"name": "laige::core::kMajor", "kind": "variable", "header": "src/laige-core/include/laige/core/version.h", "line": 15, "signature": "inline constexpr int kMajor = 0", "summary": "Semver components of the laige-core module (0 = not yet released).", "budget": null, "experimental": false}, {"name": "laige::core::kMinor", "kind": "variable", "header": "src/laige-core/include/laige/core/version.h", "line": 16, "signature": "inline constexpr int kMinor = 0", "summary": null, "budget": null, "experimental": false}, {"name": "laige::core::kPatch", "kind": "variable", "header": "src/laige-core/include/laige/core/version.h", "line": 17, "signature": "inline constexpr int kPatch = 0", "summary": null, "budget": null, "experimental": false}, diff --git a/src/laige-core/include/laige/budget_harness.h b/src/laige-core/include/laige/budget_harness.h index c210a3e..58bceb8 100644 --- a/src/laige-core/include/laige/budget_harness.h +++ b/src/laige-core/include/laige/budget_harness.h @@ -132,6 +132,7 @@ #pragma once +#include #include #include #include From 48bc5b477301e7447829cebe8fab6631ac0ff65e Mon Sep 17 00:00:00 2001 From: Pascal Severin Date: Sat, 12 Sep 2026 13:39:17 +0200 Subject: [PATCH 2/6] Trigger CI: verify windows-msvc job (ci:windows label added to this PR) From 6b3d7fe53e0e33a0f39612923a915831d441ae79 Mon Sep 17 00:00:00 2001 From: Pascal Severin Date: Sat, 12 Sep 2026 13:44:58 +0200 Subject: [PATCH 3/6] [M0-CORE-08] Fix Windows CI: unguarded getenv/fopen (C4996 fatal under /WX) After the include fix, the windows-msvc build proceeds past laige-core and fails on the remaining unguarded C4996 deprecations (fatal under the engine /WX policy, NFR-8.10): - tools/bench/laige-bench.cpp: getenv (LAIGE_BENCH_MACHINE, LAIGE_BUDGETS_PATH) and fopen (report file, mode "a") - tests/laige-core/budget_harness_tests.cpp: getenv (LAIGE_BUDGETS_PATH) Same CPP-009 platform-boundary pattern the repo already uses in logging.cpp / budget_harness.cpp / logging_tests.cpp: #if defined(_MSC_VER) switches to the CRT replacements (getenv_s, _fsopen with _SH_DENYNO) with the same lookup/open semantics; other compilers keep the standard calls. - envValue() returns the value as std::string (empty when unset); a value beyond the named kEnvValueMaxBytes (4096) is treated as unset, and the documented fallback applies (CORE-005 named bound, CORE-008 loud default instead of a truncated value). - BudgetsFilePath() now returns std::string (single call site, passed into loadBudgets(std::string_view) within one full expression - the temporary outlives the call). Audited every CI-built TU (src/tests/tools) for the C4996-prone CRT calls (getenv, fopen, tmpnam, tmpfile, strcpy, strcat, sprintf, gets, getwd, mkstemps, scandir): these were the only unguarded sites. Verified: full build + all 22 CTest entries pass; laige-bench smoke test with LAIGE_BENCH_MACHINE/LAIGE_BUDGETS_PATH set and --report append writes the report file correctly. --- tests/laige-core/budget_harness_tests.cpp | 22 +++++++++-- tools/bench/laige-bench.cpp | 48 +++++++++++++++++++---- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/tests/laige-core/budget_harness_tests.cpp b/tests/laige-core/budget_harness_tests.cpp index 734cb80..306931b 100644 --- a/tests/laige-core/budget_harness_tests.cpp +++ b/tests/laige-core/budget_harness_tests.cpp @@ -105,12 +105,28 @@ std::string WriteTempJson(const char* name, const std::string content) { return path; } +// Largest environment value the suite reads (the budgets file path; fits +// far inside the bound). Named per CORE-005; a value beyond it is treated +// as unset (the documented fallback applies). +constexpr std::size_t kEnvValueMaxBytes = 4096; + // The repo-root budgets.json, wired by CTest (ENVIRONMENT // LAIGE_BUDGETS_PATH); the fallback covers running the binary from the -// source root by hand. -const char* BudgetsFilePath() { +// source root by hand. MSVC deprecates plain getenv (C4996, fatal under +// /WX); getenv_s has the same lookup semantics (CPP-009, pattern: +// logging.cpp). +std::string BudgetsFilePath() { +#if defined(_MSC_VER) + char buf[kEnvValueMaxBytes]; + std::size_t len = 0; + if (getenv_s(&len, buf, sizeof(buf), "LAIGE_BUDGETS_PATH") != 0) + return "budgets.json"; + return (len > 0) ? std::string(buf, len) : std::string("budgets.json"); +#else const char* env = std::getenv("LAIGE_BUDGETS_PATH"); - return (env != nullptr && env[0] != '\0') ? env : "budgets.json"; + return (env != nullptr && env[0] != '\0') ? std::string(env) + : std::string("budgets.json"); +#endif } void ExpectMalformedFile(const std::string& path) { diff --git a/tools/bench/laige-bench.cpp b/tools/bench/laige-bench.cpp index 798fe95..baa7a66 100644 --- a/tools/bench/laige-bench.cpp +++ b/tools/bench/laige-bench.cpp @@ -162,6 +162,39 @@ constexpr char kCompilerId[] = "unknown"; #define LAIGE_BENCH_BUILD_TYPE "unknown" #endif +// --- Platform boundary (CPP-009, pattern: logging.cpp) ---------------------- +// +// MSVC deprecates plain getenv/fopen (C4996, fatal under the engine /WX +// policy, NFR-8.10); the Windows branch uses the CRT's documented +// replacements with the same lookup/open semantics every other supported +// compiler provides. _fsopen(_SH_DENYNO) keeps plain-fopen sharing +// semantics (no _SH_SECURE re-open denial, see logging.cpp). + +// Largest environment value this tool reads (a machine description or a +// budgets file path; both fit far inside the bound). Named per CORE-005; +// a value beyond it is treated as unset (the documented fallback applies). +constexpr std::size_t kEnvValueMaxBytes = 4096; + +#if defined(_MSC_VER) +std::string envValue(const char* name) { + char buf[kEnvValueMaxBytes]; + std::size_t len = 0; + if (getenv_s(&len, buf, sizeof(buf), name) != 0) return {}; + return std::string(buf, len); +} +std::FILE* openReportFile(const char* path, const char* mode) { + return ::_fsopen(path, mode, _SH_DENYNO); +} +#else +std::string envValue(const char* name) { + const char* v = std::getenv(name); + return (v != nullptr) ? std::string(v) : std::string(); +} +std::FILE* openReportFile(const char* path, const char* mode) { + return std::fopen(path, mode); +} +#endif + } // namespace int main(int argc, char** argv) { @@ -201,7 +234,7 @@ int main(int argc, char** argv) { // hardware/OS/compiler/build/workload). The operator may set // LAIGE_BENCH_MACHINE for the machine line; the baseline document // records the rest (docs/benchmarks/, M0-EXIT-01). - const char* envMachine = std::getenv("LAIGE_BENCH_MACHINE"); + const std::string envMachine = envValue("LAIGE_BENCH_MACHINE"); const std::string build = std::string(kCompilerId) + ", " + LAIGE_BENCH_BUILD_TYPE; @@ -225,7 +258,7 @@ int main(int argc, char** argv) { output += " build="; output += build; output += " machine="; - output += (envMachine != nullptr ? envMachine : ""); + output += envMachine; // empty when the env var is unset output += " warmup="; output += std::to_string(cfg.warmup); output += "\n"; @@ -234,10 +267,9 @@ int main(int argc, char** argv) { // full AGENTS 12 report (pass/fail + before/after + statistics). std::string budgetsPath = cfg.budgetsPath; if (budgetsPath.empty()) { - const char* envPath = std::getenv("LAIGE_BUDGETS_PATH"); - budgetsPath = (envPath != nullptr && envPath[0] != '\0') - ? std::string(envPath) - : std::string("budgets.json"); + const std::string envPath = envValue("LAIGE_BUDGETS_PATH"); + budgetsPath = + envPath.empty() ? std::string("budgets.json") : envPath; } const laige::Result table = laige::loadBudgets(budgetsPath); @@ -257,7 +289,7 @@ int main(int argc, char** argv) { BudgetReportContext ctx; ctx.workload = entry->workload.c_str(); ctx.build = build.c_str(); - ctx.machine = envMachine != nullptr ? envMachine : ""; + ctx.machine = envMachine.c_str(); // outlives the budgetCheck call below ctx.warmup = static_cast(cfg.warmup); const laige::BudgetCheckResult check = @@ -271,7 +303,7 @@ int main(int argc, char** argv) { std::fflush(stdout); if (!cfg.reportPath.empty()) { - std::FILE* f = std::fopen(cfg.reportPath.c_str(), "a"); + std::FILE* f = openReportFile(cfg.reportPath.c_str(), "a"); if (f == nullptr) { std::fprintf(stderr, "laige-bench: cannot open report file '%s'\n", cfg.reportPath.c_str()); From 89eae700528ff45dbb1b46d8e9ae592028341c6e Mon Sep 17 00:00:00 2001 From: Pascal Severin Date: Sat, 12 Sep 2026 13:59:53 +0200 Subject: [PATCH 4/6] [M0-TOOL-01] Fix Windows CI: api CTest tests cannot find the scanner under multi-config layout The six api-* CTest tests drive laige-api-scanner from generated cmake -P check scripts with a configure-time path (${CMAKE_BINARY_DIR}/bin/laige-api-scanner). That layout only matches single-config generators: under the Windows VS multi-config generator the executable lives in bin//laige-api-scanner.exe, so execute_process dies with "no such file or directory" (and api-fixture-scan additionally fails its file(READ) on the manifest that the never-running scanner was supposed to write). Fix (verified by experiment on CMake 4.4.3, Ninja Multi-Config and Unix Makefiles): - The scanner path is carried in each test's ENVIRONMENT property as $; CMake expands it per configuration in the generated CTestTestfile.cmake (Debug -> bin/Debug/..., Release -> bin/Release/..., single-config -> bin/...). - The check script reads it at test time via $ENV{LAIGE_API_SCANNER} (configure_file copies the reference verbatim; a configure-time expansion would bake in an empty/wrong value) and fails loudly when unset (CORE-008). - kEnvValueMaxBytes moved inside the MSVC #if branches in laige-bench.cpp and budget_harness_tests.cpp: outside them it is an unused namespace-scope constant, which -Wunused-const-variable makes fatal under -Werror on Clang (NFR-8.10, caught in the ASan tree). Verified: all 22 CTest entries pass in build, build-asan, build-tsan, build-clang, build-shared, and build-clang-shared; the multi-config experiment confirmed per-config resolution and single-config parity. --- tests/api/CMakeLists.txt | 12 ++++++++++-- tests/api/expect-api-result.cmake.in | 15 ++++++++++++++- tests/laige-core/budget_harness_tests.cpp | 10 +++++----- tools/bench/laige-bench.cpp | 4 +++- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/tests/api/CMakeLists.txt b/tests/api/CMakeLists.txt index 6f45908..2e8b049 100644 --- a/tests/api/CMakeLists.txt +++ b/tests/api/CMakeLists.txt @@ -174,7 +174,6 @@ function(laige_add_api_test name expect_exit root mode file) " string(APPEND _problems \"output missing '<${_esc}'>; \")\n" "endif()\n") endforeach() - set(SCANNER "${CMAKE_BINARY_DIR}/bin/laige-api-scanner") set(CMD "${_cmd}") set(EXPECT_EXIT "${expect_exit}") set(OUT_FILE "") @@ -186,6 +185,13 @@ function(laige_add_api_test name expect_exit root mode file) "${CMAKE_CURRENT_BINARY_DIR}/${name}.cmake" @ONLY) add_test(NAME ${name} COMMAND ${CMAKE_COMMAND} -P "${CMAKE_CURRENT_BINARY_DIR}/${name}.cmake") + # The scanner path is carried in the test environment, not in the + # command or baked into the script: $ resolves per + # configuration under multi-config generators (the Windows VS layout + # puts the executable in bin//), while a configure-time path + # only matches the single-config layout. + set_tests_properties(${name} PROPERTIES + ENVIRONMENT "LAIGE_API_SCANNER=$") endfunction() # The scan test also compares the written manifest byte-for-byte against @@ -194,7 +200,6 @@ set(_scan_out "${CMAKE_CURRENT_BINARY_DIR}/fixture-api.json") set(_scan_root "${FX}/api") set(_scan_expected "${CMAKE_CURRENT_BINARY_DIR}/expected-fixture-api.json") set(_cmd "\"--root\" \"${_scan_root}\" \"--out\" \"${_scan_out}\"") -set(SCANNER "${CMAKE_BINARY_DIR}/bin/laige-api-scanner") set(CMD "${_cmd}") set(EXPECT_EXIT "0") set(OUT_FILE "${_scan_out}") @@ -203,6 +208,9 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/expect-api-result.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/api-fixture-scan.cmake" @ONLY) add_test(NAME api-fixture-scan COMMAND ${CMAKE_COMMAND} -P "${CMAKE_CURRENT_BINARY_DIR}/api-fixture-scan.cmake") +# Same ENVIRONMENT wiring as laige_add_api_test (see its comment). +set_tests_properties(api-fixture-scan PROPERTIES + ENVIRONMENT "LAIGE_API_SCANNER=$") laige_add_api_test(api-check-fresh 0 "${FX}/api" check "${CMAKE_CURRENT_BINARY_DIR}/expected-fixture-api.json" diff --git a/tests/api/expect-api-result.cmake.in b/tests/api/expect-api-result.cmake.in index 3eef20a..53aba6b 100644 --- a/tests/api/expect-api-result.cmake.in +++ b/tests/api/expect-api-result.cmake.in @@ -12,9 +12,22 @@ # The script exits non-zero (failing the CTest test) on any mismatch and # prints the full scanner output for diagnosis. +# The scanner executable arrives through the test's ENVIRONMENT property +# (LAIGE_API_SCANNER = $, set in +# tests/api/CMakeLists.txt): CMake resolves it per configuration under +# multi-config generators (bin//laige-api-scanner.exe on the +# Windows VS generator), where a path baked in at configure time only +# matches the single-config layout. Read it at test time, never at +# configure time ($ENV{...} in a configure-time string expands early). +set(_scanner "$ENV{LAIGE_API_SCANNER}") +if(_scanner STREQUAL "") + message(FATAL_ERROR + "LAIGE_API_SCANNER is not set: the test's ENVIRONMENT property must " + "carry the scanner path (tests/api/CMakeLists.txt)") +endif() set(_cmd @CMD@) execute_process( - COMMAND @SCANNER@ ${_cmd} + COMMAND "${_scanner}" ${_cmd} RESULT_VARIABLE _rc OUTPUT_VARIABLE _out ERROR_VARIABLE _err diff --git a/tests/laige-core/budget_harness_tests.cpp b/tests/laige-core/budget_harness_tests.cpp index 306931b..b77c027 100644 --- a/tests/laige-core/budget_harness_tests.cpp +++ b/tests/laige-core/budget_harness_tests.cpp @@ -105,11 +105,6 @@ std::string WriteTempJson(const char* name, const std::string content) { return path; } -// Largest environment value the suite reads (the budgets file path; fits -// far inside the bound). Named per CORE-005; a value beyond it is treated -// as unset (the documented fallback applies). -constexpr std::size_t kEnvValueMaxBytes = 4096; - // The repo-root budgets.json, wired by CTest (ENVIRONMENT // LAIGE_BUDGETS_PATH); the fallback covers running the binary from the // source root by hand. MSVC deprecates plain getenv (C4996, fatal under @@ -117,6 +112,11 @@ constexpr std::size_t kEnvValueMaxBytes = 4096; // logging.cpp). std::string BudgetsFilePath() { #if defined(_MSC_VER) + // Largest environment value the suite reads (the budgets file path; + // fits far inside the bound). Named per CORE-005; a value beyond it is + // treated as unset (the documented fallback applies). MSVC-only for the + // same reason as above (CORE-010: no unused symbols under -Werror). + constexpr std::size_t kEnvValueMaxBytes = 4096; char buf[kEnvValueMaxBytes]; std::size_t len = 0; if (getenv_s(&len, buf, sizeof(buf), "LAIGE_BUDGETS_PATH") != 0) diff --git a/tools/bench/laige-bench.cpp b/tools/bench/laige-bench.cpp index baa7a66..e6ed03a 100644 --- a/tools/bench/laige-bench.cpp +++ b/tools/bench/laige-bench.cpp @@ -170,12 +170,14 @@ constexpr char kCompilerId[] = "unknown"; // compiler provides. _fsopen(_SH_DENYNO) keeps plain-fopen sharing // semantics (no _SH_SECURE re-open denial, see logging.cpp). +#if defined(_MSC_VER) // Largest environment value this tool reads (a machine description or a // budgets file path; both fit far inside the bound). Named per CORE-005; // a value beyond it is treated as unset (the documented fallback applies). +// MSVC-only: getenv_s needs a caller-sized buffer, so the constant has no +// use outside this branch (CORE-010: no unused symbols under -Werror). constexpr std::size_t kEnvValueMaxBytes = 4096; -#if defined(_MSC_VER) std::string envValue(const char* name) { char buf[kEnvValueMaxBytes]; std::size_t len = 0; From b5bbacb7f9f238fa111bce09ecf4b13c573bec28 Mon Sep 17 00:00:00 2001 From: Pascal Severin Date: Sat, 12 Sep 2026 14:22:52 +0200 Subject: [PATCH 5/6] [M0-TOOL-01] Fix Windows CI: api-check-fresh stale on CRLF fixture (normalize CRLF to LF in --check) After the scanner-path fix, the Windows windows-msvc job's ctest left one failure: api-check-fresh reported "stale (0 difference(s))". Root cause: the canonical manifest form is LF (the serializer emits LF; .gitattributes pins eol=lf on every checkout). But CMake's file(WRITE) opens a text-mode stream on Windows (cmFileCommand: ofstream without ios::binary), so the CMake-generated test fixtures (expected-fixture-api.json, ...) land on disk with CRLF endings. The api-fixture-scan test survived because CMake's file(READ) normalizes CRLF->LF on read; the scanner's --check reads the file in binary mode, so its byte compare saw CRLF vs LF, while the symbol-level diff was 0 (CRLF is JSON whitespace) - hence "stale (0 difference(s))". Fix: in runCheck, fold CRLF pairs to LF in the read file before the byte compare (a lone \r is NOT folded, so a genuinely corrupted file still fails, CORE-008). The regeneration itself is unchanged and stays byte-identical LF (NFR-13.4); contract text (header comment, --help) updated in the same change (CORE-006). Verified locally (CMake 4.4.3): LF copy -> exit 0, CRLF copy -> exit 0, lone-CR copy -> exit 1 "stale", stale fixture -> exit 1 with the 2 symbol diffs, missing file -> exit 2; all 22 CTest entries pass in build, build-asan, build-tsan, build-clang, build-shared, and build-clang-shared. --- tools/api/laige-api.cpp | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tools/api/laige-api.cpp b/tools/api/laige-api.cpp index 7428946..8b876da 100644 --- a/tools/api/laige-api.cpp +++ b/tools/api/laige-api.cpp @@ -16,7 +16,9 @@ // --root REPO_ROOT Repository root (default: "."). // --out FILE Write the regenerated manifest to FILE. // --check FILE Regenerate in memory and compare with FILE: -// byte-identical -> exit 0; stale -> exit 1 with a +// identical -> exit 0 (CRLF line endings in FILE are +// normalized to LF first - the canonical form is LF, +// see .gitattributes); stale -> exit 1 with a // symbol-level diff; unreadable/invalid FILE -> exit 2. // // Exit codes: 0 = OK · 1 = stale manifest (--check) · 2 = error (usage, @@ -1722,6 +1724,26 @@ int runCheck(const std::string& checkFile, const std::string& manifestText, std::cout << "laige-api: error: " << err << std::endl; return 2; } + // Line-ending normalization: the manifest is canonically LF (the + // serializer emits LF; the repo's .gitattributes pins eol=lf on every + // checkout). CRLF can reach a checked-in copy only as a Windows tooling + // artifact (text-mode CMake file(WRITE) test fixtures, editor saves), so + // normalize CRLF -> LF before the byte compare. A lone \r is NOT + // normalized, so a genuinely corrupted file still fails (CORE-008). + { + std::string norm; + norm.reserve(oldText.size()); + for (size_t i = 0; i < oldText.size(); ++i) { + if (oldText[i] == '\r' && i + 1 < oldText.size() && + oldText[i + 1] == '\n') { + norm += '\n'; // fold the CRLF pair to one LF + ++i; // the paired \n belongs to the fold - skip it + } else { + norm += oldText[i]; + } + } + oldText = std::move(norm); + } if (oldText == manifestText) { std::cout << "laige-api: OK — " << checkFile << " is up to date (" << symbols.size() << " symbol(s) from the " @@ -1828,7 +1850,8 @@ void printUsage(std::ostream& os) { << " --root REPO_ROOT repository root (default: \".\")\n" << " --out FILE write the regenerated manifest to FILE\n" << " --check FILE compare the regenerated manifest with FILE:\n" - << " exit 0 if byte-identical, exit 1 if stale\n" + << " exit 0 if identical (CRLF normalized to LF),\n" + << " exit 1 if stale\n" << "\n" << "Exit codes: 0 = OK · 1 = stale manifest · 2 = error\n"; } From fe27b0f25554987fb624355b806a7863d5b2a1e1 Mon Sep 17 00:00:00 2001 From: Pascal Severin Date: Sat, 12 Sep 2026 14:26:02 +0200 Subject: [PATCH 6/6] Trigger CI: verify macOS lanes (ci:macos label added to this PR)