diff --git a/.github/workflows/spec-guards.yml b/.github/workflows/spec-guards.yml index 905b15ca7a..51f2577b00 100644 --- a/.github/workflows/spec-guards.yml +++ b/.github/workflows/spec-guards.yml @@ -119,15 +119,15 @@ jobs: # gen/c/claw/gates.c is committed (force-added; gen/ is ignored but # tracked) and the claw-drift step in gate-topology.yml feeds those # COMMITTED bytes -- nothing else holds them to their spec. An edit to - # specs/claw/gates.t27 without a regen would drift silently: the - # committed C would keep answering for a spec it no longer lowers. - # Same shape as t27o-golden's check mode (#8996): regenerate, then - # diff --exit-code. - - name: committed gen/c/claw/gates.c and t27f_gate.c are what their specs lower to today + # specs/claw/gates.t27 without a regen would drift silently. So would the IGLA board's + # own_model event, which a Railway cron reposts from master's committed JSON (#10321). + # Same shape as t27o-golden's check mode (#8996): regenerate, then diff --exit-code. + - name: committed gen/c/claw/*.c and gen/json/igla/own_model_board.json are what their specs lower to today run: | ./target/release/t27c gen-c specs/claw/gates.t27 > gen/c/claw/gates.c ./target/release/t27c gen-c specs/claw/t27f_gate.t27 > gen/c/claw/t27f_gate.c - git diff --exit-code -- gen/c/claw/gates.c gen/c/claw/t27f_gate.c + ./target/release/t27c gen-json specs/igla/own_model_board.t27 > gen/json/igla/own_model_board.json + git diff --exit-code -- gen/c/claw/gates.c gen/c/claw/t27f_gate.c gen/json/igla/own_model_board.json # #7565: test only the specs this change can affect. specs/ci/affected.t27 picks them (the # changed specs and every spec that imports one through `use`, transitively); the graph is the diff --git a/.trinity/seals/crons_cron_t27_own_model_board.json b/.trinity/seals/crons_cron_t27_own_model_board.json new file mode 100644 index 0000000000..ba153ffbc6 --- /dev/null +++ b/.trinity/seals/crons_cron_t27_own_model_board.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+79126608b", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:85795a37414e093c16332554b79f7de01d40e6a4111ad2bcff55c6c51dc51c59", + "gen_hash_rust": "sha256:aff9afd3ae6f0a87fdff5b0d722f17b27eb20edf9213b7f80dd6a966d9509aaa", + "gen_hash_verilog": "sha256:cd58d73af401293da1e1d909746c535f792224a36b2ecee76a653b450e8c8ff7", + "gen_hash_zig": "sha256:6d8b5e3d207ebd83d018fcb4949e80f69e478c5bbd3d769e49068d98d55010a0", + "module": "cron_t27_own_model_board", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T08:54:26Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:3032ee2782ec57a66af46e64bb269d5185a8a08bacaeb2db26cb48cbe1f7f921", + "spec_path": "specs/crons/t27-own-model-board.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 3, + "total": 3, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/gen/json/igla/own_model_board.json b/gen/json/igla/own_model_board.json new file mode 100644 index 0000000000..29e760a559 --- /dev/null +++ b/gen/json/igla/own_model_board.json @@ -0,0 +1 @@ +{"job":"own_model","stage":"own_model","msg":"1 published, 17 verified, 8 drafts, 1 gaps; 61%","progress":0.61,"metrics":{"lens_pct":[65,70,48],"cells":[{"status":4,"pct":60},{"status":4,"pct":70},{"status":4,"pct":80},{"status":4,"pct":80},{"status":4,"pct":60},{"status":4,"pct":100},{"status":3,"pct":50},{"status":3,"pct":90},{"status":0,"pct":0},{"status":4,"pct":100},{"status":4,"pct":100},{"status":4,"pct":20},{"status":4,"pct":80},{"status":4,"pct":65},{"status":4,"pct":95},{"status":3,"pct":75},{"status":3,"pct":55},{"status":3,"pct":40},{"status":4,"pct":70},{"status":4,"pct":15},{"status":5,"pct":60},{"status":4,"pct":40},{"status":4,"pct":100},{"status":4,"pct":60},{"status":3,"pct":30},{"status":3,"pct":10},{"status":3,"pct":50}]}} diff --git a/specs/crons/t27-own-model-board.t27 b/specs/crons/t27-own-model-board.t27 new file mode 100644 index 0000000000..2dd41f3592 --- /dev/null +++ b/specs/crons/t27-own-model-board.t27 @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/crons/t27-own-model-board.t27 -- cron railway-cron/t27/own-model-board +; Source of truth for the job card on t27.ai (#/crons?cron=railway-cron/t27/own-model-board). +; The schedule lives on Railway service own-model-board in project igla-judge (the board's project, +; so the board's private port is reachable); the payload lives here, in gHashTag/t27 (#10321). +; What one fire does, in words: fetch master's gen/json/igla/own_model_board.json from +; raw.githubusercontent.com and POST it, unchanged, to http://board.railway.internal:8081/event. +; That file is `t27c gen-json specs/igla/own_model_board.t27`, held to its spec by spec-guards.yml +; (regenerate, then git diff --exit-code). So a spec change reaches the board at the next fire, +; and a board redeploy, which empties its live map, is repaired at the next fire too. +; The service is the public image curlimages/curl and holds no variable: the private port takes +; no token, and the public repository needs none. +; A cron CONTROLS skills: RUNS names the skill IDs it launches. It is empty +; unless the source evidently invokes one -- nothing here is inferred from a name. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module cron_t27_own_model_board; + +pub const KIND : str = "cron"; +pub const ID : str = "railway-cron/t27/own-model-board"; +pub const NAME : str = "own-model-board"; +pub const HOST : str = "railway-cron"; +pub const REPO : str = "t27"; +pub const SERVICE : str = "own-model-board"; +pub const SUMMARY_EN : str = "Reposts the own_model event (the 27-cell map of specs/igla/own_model.t27) to the IGLA board, from the JSON t27c generates on master."; +; Four fires a day: an event that has not changed only refreshes its receive time, and the +; board keeps the last 200 events, so a tighter period would crowd out every other job. +pub const SCHEDULE : str = "17 */6 * * *"; +pub const TZ : str = "UTC"; +pub const RUNS : [0]str = []; +pub const RUNS_NOTE : str = "posts one board event; invokes no skill"; +pub const ENABLED : bool = true; +pub const NOTE : str = "Railway project igla-judge, service own-model-board. The schedule is set on the service, not in a checkout; the payload is gen/json/igla/own_model_board.json on master."; +pub const ON_FAILURE : str = "log"; +pub const CONTROL : str = "railway-dashboard"; + +; The card held to the schema of specs/crons/README.md: ID is //; HOST, +; ON_FAILURE and CONTROL come from fixed vocabularies; RUNS, the control link, is empty unless +; the source evidently invokes a skill, and RUNS_NOTE then says why. +test id_is_host_repo_and_slug { + // HOST is one of the five hosts the README names. + assert HOST == "github-actions" or HOST == "inngest" or HOST == "railway-cron" or HOST == "timer" or HOST == "launchd"; + // ID opens with HOST, a `/`, REPO and a `/`. + var i : usize = 0; + while (i < HOST.len) { + assert ID[i] == HOST[i]; + i = i + 1; + } + assert ID[HOST.len] == '/'; + var r : usize = 0; + while (r < REPO.len) { + assert ID[HOST.len + 1 + r] == REPO[r]; + r = r + 1; + } + assert ID[HOST.len + 1 + REPO.len] == '/'; + // The slug after it is one path segment, not empty, and names the Railway service. + assert ID.len == HOST.len + REPO.len + 2 + SERVICE.len; + var s : usize = 0; + while (s < SERVICE.len) { + assert ID[HOST.len + REPO.len + 2 + s] == SERVICE[s]; + assert SERVICE[s] != '/' and SERVICE[s] != ' '; + s = s + 1; + } +} +test vocabularies_and_the_control_link { + assert KIND == "cron"; + assert ON_FAILURE == "issue" or ON_FAILURE == "log" or ON_FAILURE == "unknown"; + assert CONTROL == "github-actions-dispatch" or CONTROL == "railway-dashboard" or CONTROL == "inngest-dashboard" or CONTROL == "code-only"; + // An empty RUNS carries its reason in RUNS_NOTE. + assert RUNS.len > 0 or RUNS_NOTE.len > 0; +} +test the_schedule_is_four_fires_a_day { + // Five fields separated by single spaces, no blank field. + var spaces : usize = 0; + var k : usize = 0; + while (k < SCHEDULE.len) { + if (SCHEDULE[k] == ' ') { + spaces = spaces + 1; + assert k > 0 and k + 1 < SCHEDULE.len and SCHEDULE[k + 1] != ' '; + } + k = k + 1; + } + assert spaces == 4; + // A fixed minute, not `*`: one fire per matching hour. Hour field `*/6`: four a day. + assert SCHEDULE[0] >= '0' and SCHEDULE[0] <= '9'; + assert SCHEDULE[3] == '*' and SCHEDULE[4] == '/' and SCHEDULE[5] == '6'; + assert TZ == "UTC"; + assert ENABLED; +}