Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 14 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,20 @@ jobs:
# Build with -Dci=true to skip GUI targets requiring raylib
zig build -Dci=true

# P0.4: Memory leak detection via Zig's General Purpose Allocator (GPA).
# Any memory leaks will cause the test to fail with a "leaked" error message.
# This is a HARD GATE - the build will fail if any tests leak memory.
# The leak checking is enabled by using std.testing.allocator in tests.
# Additional runtime leak checking can be enabled with --zig-lib-dir.
- name: Test (with leak detection)
run: |
# Run tests with Zig's built-in leak detection
# Tests using std.testing.allocator will automatically detect leaks
zig build test -Dci=true 2>&1 | tee test-output.txt
# Check for any memory leak indicators
if grep -i "leak\|memory.*leak\|allocation.*leak" test-output.txt; then
echo "::error::Memory leaks detected in tests"
exit 1
fi
# P0.4: Memory leak detection via Zig's General Purpose Allocator (GPA) is kept: a
# leak fails this step as before.
#
# The test step is judged, not piped. It used to run `zig build test -Dci=true 2>&1 | tee`
# under bash without pipefail, so its exit code was tee's: on main five test roots do not
# compile -- src/trinity.zig, the library's own test root, among them -- and the step was
# green on every push while its summary said "124/135 steps succeeded; 5 failed" (#616;
# #987 is the tee fix alone). tools/reproduce.py reads the exit code from the process and
# holds the result to specs/reproduce/headless.t27: exactly the roots that file names as
# blocked may fail, a sixth fails this step, and so does a blocked root that compiles again
# (take it off the list); a failed test, a leak, fewer tests or fewer run steps fail it too.
# The record of the run is test-output.json.
- name: Test (judged against specs/reproduce/headless.t27)
run: python3 tools/reproduce.py test --reuse-cache --out test-output.json

- name: Format check
run: |
Expand Down
59 changes: 59 additions & 0 deletions .github/workflows/reproduce.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Reproduce the headless profile

# S12 of #988 (#989): the declared headless profile, built and tested from a clean
# clone on a runner with no cache, and judged against specs/reproduce/headless.t27.
# tools/reproduce.py points ZIG_GLOBAL_CACHE_DIR and ZIG_LOCAL_CACHE_DIR at a new
# empty directory and setup-zig keeps no cache, so nothing built before is reused.
# The record of the run -- each command with its exit code, the compile failures by
# root, the run steps, every compiled test root and every install with its sha256 --
# is uploaded as the artifact reproduce-headless.

on:
pull_request:
branches: [main]
paths:
- "build.zig"
- "build.zig.zon"
- "src/**"
- "external/**"
- "deploy/**/*.zig"
- "tools/reproduce.py"
- "specs/reproduce/**"
- ".github/workflows/reproduce.yml"
push:
branches: [main]
schedule:
- cron: "23 4 * * 1"
workflow_dispatch:

jobs:
headless:
name: Headless profile from a clean clone
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4

- name: Setup Zig, no cache
uses: mlugg/setup-zig@v2
with:
version: 0.15.2
use-cache: false

# A gate nobody has seen fail is not a gate: the judge is run on planted records
# first -- a swallowed exit code, a new compile failure, a blocked root that compiles,
# a failed test, a leak, a shrunken graph, an empty run step, a missing install --
# and each must be reported.
- name: Negative control
run: python3 tools/reproduce.py --self-check

- name: Build and test the headless profile with fresh caches
run: python3 tools/reproduce.py headless --out reproduce-headless.json

- name: Upload the record
if: always()
uses: actions/upload-artifact@v4
with:
name: reproduce-headless
path: reproduce-headless.json
if-no-files-found: error
126 changes: 126 additions & 0 deletions specs/reproduce/headless.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// SPDX-License-Identifier: Apache-2.0
; specs/reproduce/headless.t27 -- the headless profile of gHashTag/trinity, reproduced from a clean clone
; S12 of gHashTag/trinity#988 (gHashTag/trinity#989): a set of specifications is not a recreated project until
; a clean checkout builds and tests the declared profile, and a failure anywhere fails the gate. This module is
; that profile, measured; tools/reproduce.py runs it and judges every run against it (the command is
; `python3 tools/reproduce.py headless`, the CI job is .github/workflows/reproduce.yml).
; MEASURED at gHashTag/trinity main afc9d38435ad01c48af6c33c311da422c38f046e (2026-09-30) with Zig 0.15.2, twice:
; Linux aarch64 in a container with empty caches, and the Build & Test job of CI run 36875247882 (Linux x86_64).
; Both print the same test summary, "124/135 steps succeeded; 5 failed; 2510/2510 tests passed", and until
; this module CI reported that summary as a success: its test step was `zig build test -Dci=true 2>&1 | tee`
; under bash without pipefail, so the step's exit code was tee's (gHashTag/trinity#616, the tee fix alone is
; gHashTag/trinity#987). Five test roots do not compile, the library's own src/trinity.zig among them, and
; their tests have not run since; they are BLOCKED below, each with the error and the capability card it
; holds back, and the gate fails the day one of them compiles until it is taken off the list.
; Not a claim about any profile but headless; not a claim that the 2510 tests that ran test enough.
; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY

module reproduce_headless;

pub const KIND : str = "reproduce-profile";
pub const ID : str = "reproduce/headless";
pub const PROFILE : str = "headless";
pub const MEASURED_AT : str = "afc9d38435ad01c48af6c33c311da422c38f046e";
pub const ZIG : str = "0.15.2";
; The documented clean-clone commands. tools/reproduce.py replaces the leading zig with the binary it is given
; and, unless asked to reuse caches, points ZIG_GLOBAL_CACHE_DIR and ZIG_LOCAL_CACHE_DIR at a new empty
; directory, so nothing a developer built before is reused.
pub const BUILD : str = "zig build -Dci=true --summary all";
pub const TEST : str = "zig build test -Dci=true --summary all";
pub const CLEAN_CLONE : str = "git clone https://github.com/gHashTag/trinity && cd trinity && python3 tools/reproduce.py headless";
; The four packages build.zig.zon pins, exactly: zig verifies a package against its hash, so a changed pin is
; a different dependency that builds just as quietly. Changing one is a decision made here, not only there.
pub const DEPENDENCIES : [4]str = ["emsdk", "raylib", "zig_hdc", "zig_golden_float"];
pub const DEPENDENCY_URLS : [4]str = ["git+https://github.com/emscripten-core/emsdk?ref=4.0.9#3bcf1dcd01f040f370e10fe673a092d9ed79ebb5", "git+https://github.com/raylib-zig/raylib-zig#cd71c85d571027ac8033357f83b124ee051825b3", "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/gHashTag/zig-hdc/archive/b73b2fa29874f4e30297f167a6ea8a3c2ded9d32.tar.gz", "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/gHashTag/zig-golden-float/archive/e7ce32885de2a8c50b7b6a3030d0592202145dd1.tar.gz"];
pub const DEPENDENCY_HASHES : [4]str = ["N-V-__8AAJl1DwBezhYo_VE6f53mPVm00R-Fk28NPW7P14EQ", "raylib_zig-5.6.0-dev-KE8REENOBQC-m5nK7M2b5aKSIubJPbPLUYcRhT7aT3RN", "zig_hdc-0.1.0-6NS2Eg99FQCG62j7TmpGwVTnq07LhNb1FUHrwUacgWdO", "golden_float-2.1.0-h7LKhUUNCwAtKVHQ56wjridCZVwXY7_oxT2hXD9xCDdF"];
; The build: every step succeeds, and these 46 files are installed under zig-out (45 executables and the
; static library) -- the INSTALLED_BY_DEFAULT of the S01 manifest (specs/trinity/project.t27 in gHashTag/t27).
pub const BUILD_STEPS_MIN : u32 = 94;
pub const INSTALLS : [46]str = [
"bin/agent-entrypoint", "bin/arena", "bin/b2t", "bin/background-agent-api",
"bin/beal", "bin/bench-compress", "bin/bench-core", "bin/claude-ui",
"bin/example-memory", "bin/example-sequence", "bin/example-vm", "bin/firebird",
"bin/fluent", "bin/forge", "bin/hslm-train", "bin/mu-agent",
"bin/needle-mcp", "bin/pas-demo", "bin/phi-loop", "bin/photon-terminal",
"bin/pipeline-guard", "bin/quantum", "bin/railway-create-service", "bin/railway-redeploy",
"bin/railway-set-builder", "bin/railway-set-dockerfile", "bin/railway-set-startcmd", "bin/railway-trigger-redeploy",
"bin/railway-update-service", "bin/ralph-agent", "bin/ralph-hook", "bin/scholar-agent",
"bin/sota-report", "bin/swe-entrypoint", "bin/tri", "bin/tri-api",
"bin/tri-bot", "bin/trinity-cli", "bin/trinity-hybrid", "bin/trinity-mcp",
"bin/trinity-node", "bin/trinity-query", "bin/trinity-search", "bin/vibee",
"bin/vibee-self-improve", "lib/libtrinity.a"
];
; The test graph: the floors it may not drop below. A graph that shrank is a partial graph, and a run step
; that ran no test is not a passing one: one does today, the step of external/zig-hslm/src/root.zig, the only
; compiled test root without a test declaration.
pub const TEST_STEPS_MIN : u32 = 135;
pub const TESTS_PASSED_MIN : u32 = 2510;
pub const RUN_STEPS_WITH_TESTS_MIN : u32 = 61;
pub const EMPTY_RUN_STEPS_MAX : u32 = 1;
; Every root the test step compiles, read from `--verbose` with empty caches. The five BLOCKED ones are here too:
; the step still tries them, and a root that leaves this list is a test that left the graph.
pub const TEST_ROOTS : [67]str = [
"external/zig-hslm/src/root.zig", "src/agent_mu/production_hardening_test.zig",
"src/agent_mu/production_test.zig", "src/agent_mu/swarm_collaboration.zig",
"src/api/unified_server.zig", "src/arena/main.zig",
"src/beal.zig", "src/c_api.zig",
"src/depin/network.zig", "src/e2e_test.zig",
"src/firebird/b2t_integration.zig", "src/firebird/depin.zig",
"src/firebird/extension_wasm.zig", "src/firebird/wasm_parser.zig",
"src/igla/trace.zig", "src/queen_api.zig",
"src/trinity.zig", "src/trinity_node/auto_repair.zig",
"src/trinity_node/bandwidth_aggregator.zig", "src/trinity_node/connection_pool.zig",
"src/trinity_node/cross_shard_tx.zig", "src/trinity_node/crypto.zig",
"src/trinity_node/dynamic_erasure.zig", "src/trinity_node/erasure_repair.zig",
"src/trinity_node/file_encoder.zig", "src/trinity_node/galois.zig",
"src/trinity_node/graceful_shutdown.zig", "src/trinity_node/incentive_slashing.zig",
"src/trinity_node/integration_test.zig", "src/trinity_node/manifest_dht.zig",
"src/trinity_node/metrics_http.zig", "src/trinity_node/network_stats.zig",
"src/trinity_node/node_reputation.zig", "src/trinity_node/parallel_saga.zig",
"src/trinity_node/peer_latency.zig", "src/trinity_node/prometheus_http.zig",
"src/trinity_node/prometheus_metrics.zig", "src/trinity_node/proof_of_storage.zig",
"src/trinity_node/protocol.zig", "src/trinity_node/reed_solomon.zig",
"src/trinity_node/region_router.zig", "src/trinity_node/region_topology.zig",
"src/trinity_node/remote_storage.zig", "src/trinity_node/repair_rate_limiter.zig",
"src/trinity_node/reputation_consensus.zig", "src/trinity_node/rs_repair.zig",
"src/trinity_node/saga_coordinator.zig", "src/trinity_node/semantic_index.zig",
"src/trinity_node/shard_manager.zig", "src/trinity_node/shard_rebalancer.zig",
"src/trinity_node/shard_scrubber.zig", "src/trinity_node/slashing_escrow.zig",
"src/trinity_node/stake_delegation.zig", "src/trinity_node/storage.zig",
"src/trinity_node/storage_discovery.zig", "src/trinity_node/token_staking.zig",
"src/trinity_node/transaction_wal.zig", "src/trinity_node/vsa_shard_encoder.zig",
"src/trinity_node/vsa_shard_locks.zig", "src/trinity_node/wal_disk.zig",
"src/vibeec/codegen_tests.zig", "src/vibeec/kg_pipeline.zig",
"src/vibeec/kg_sync.zig", "src/vibeec/prefix_cache_completion.zig",
"src/vibeec/sym_005_demo.zig", "src/vibeec/triples_parser.zig",
"src/vm.zig"
];
; The roots that do not compile at MEASURED_AT, what the compiler says, and the capability card of the S01
; manifest each one holds back. Exactly these may fail; one more fails the gate, and one fewer fails it too.
pub const BLOCKED_ROOTS : [5]str = ["src/c_api.zig", "src/e2e_test.zig", "src/queen_api.zig", "src/trinity.zig", "src/vm.zig"];
pub const BLOCKED_REASONS : [5]str = [
"src/c_api.zig:270:1: expected statement, found 'a document comment' -- the file does not parse; S05 of the epic recorded the same at the S01 pin",
"src/packed_trit.zig:1:1: unable to load 'packed_trit.zig': FileNotFound -- src/e2e_test.zig:21 imports a file removed in 42490a222 (#517)",
"lib/std/c.zig and lib/std/heap.zig:128: the C allocator without libc (4 errors) -- the queen_api test module does not set link_libc, as the main test module does",
"src/hybrid.zig:4 and src/vsa.zig:7: no module named 'zig-hdc-vsa' (the test module provides it as hdc_vsa); src/science.zig:53, 143, 161, 179, 204: expected optional type, found '[59049]i8' (7 errors)",
"src/vm.zig:1150:15: expected optional type, found '[59049]i8'; src/vsa_jit.zig:117:26: member function expected 1 argument(s), found 2"
];
pub const BLOCKED_CAPABILITIES : [5]str = ["trinity/abi.c-api", "trinity/examples.zig", "trinity/queen.lib", "trinity/lib.trinity", "trinity/lib.trinity"];
pub const EVIDENCE : str = "tools/reproduce.py writes zig-out/reproduce/<command>.json: toolchain, head, cache freshness, each command with its exit code and summary, the compile failures by root, the run steps, the compiled roots, every install with its sha256, and the violations";
pub const ENABLED : bool = true;

test the_profile_is_headless_and_pinned {
assert PROFILE == "headless";
assert ZIG == "0.15.2";
assert BUILD_STEPS_MIN == 94;
}

test the_floors_hold_together {
assert TESTS_PASSED_MIN == 2510;
assert RUN_STEPS_WITH_TESTS_MIN + EMPTY_RUN_STEPS_MAX + 5 == 67;
assert TEST_STEPS_MIN > RUN_STEPS_WITH_TESTS_MIN;
}

invariant blocked_roots_are_few {
assert EMPTY_RUN_STEPS_MAX <= 1;
}
Loading
Loading