diff --git a/.trinity/seals/t27os_T27osInstall.json b/.trinity/seals/t27os_T27osInstall.json new file mode 100644 index 0000000000..ae4d9e32fe --- /dev/null +++ b/.trinity/seals/t27os_T27osInstall.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+c948f4df7", + "closure": [ + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:968755ec1917f766eb2cdd0ab4c0c48aef028a0e7d69e191911695e925b286ad" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:1f41a6bb4d9ec65c187790eb458760fb7467780b2e8651dad5941a26dabc5936", + "gen_hash_rust": "sha256:3b70f85eb46ff31a0e7b52cf82f209121d124884eabd41e7e31cd62436fe0757", + "gen_hash_verilog": "sha256:1d90aa61656131b78be2afd49581c00ed5e1ab82db2adac6686132b357eacc98", + "gen_hash_zig": "sha256:e6999d57214ce4e9e2adadbe99b8f51982bcc17f1d7bf8b776d24ddc8bf5945b", + "module": "T27osInstall", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-19T14:01:03Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:7315ca7fc6c30d92907c3311015ec5511e4d76622073032f9211ffe601a172c4", + "spec_path": "specs/t27os/install.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 16, + "total": 16, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/Makefile b/Makefile index 1c18c936d8..ab98606847 100644 --- a/Makefile +++ b/Makefile @@ -1,53 +1,52 @@ -# Minimal top-level convenience targets for t27. -# -# This Makefile is intentionally thin: it only wraps existing scripts so common -# developer checks have a memorable entry point. It is NOT wired into CI (the -# required checks are the ruleset's answer: -# gh api repos/gHashTag/t27/rules/branches/master) and adds no build logic of its own. -# -# Anchor: phi^2 + phi^-2 = 3 - -.PHONY: help verify seal-check seal install-hooks warnings-baseline - -# Default target: list what is available. +# T27 OS: one installer, built with make (#11044), and the developer shortcuts below it. Thin on +# purpose: the install decisions (platform, prefix, version pin, idempotence, uninstall list, dry run, +# opt-in flags) are specs/t27os/install.t27; this file names the same files and flags and carries +# them out with cargo and install(1). Not wired into CI (the required checks are the ruleset's answer: +# gh api repos/gHashTag/t27/rules/branches/master). Anchor: phi^2 + phi^-2 = 3 + +PREFIX ?= /usr/local +T27B := $(filter arm64 aarch64,$(shell uname -m)) +LIST := $(PREFIX)/share/t27/install.list +PAIRS := scripts/tri:bin/tri target/release/t27c:bin/t27c target/release/tri:target/release/tri target/release/t27c:target/release/t27c $(if $(T27B),target/release/t27b:bin/t27b) $(if $(filter on,$(CLAW)),$(foreach f,specs/claw/service.t27 specs/claw/text.t27 .trinity/seals/claw_ClawService.json .trinity/seals/claw_ClawText.json,$(f):share/t27/claw/$(f))) + +.PHONY: help install uninstall verify seal-check seal warnings-baseline + +## make install build and install tri, t27c and t27b under PREFIX (/usr/local); PIN= insists on that t27c +## make -n install print the plan and change nothing +## make install PREFIX=$HOME/.local install under a directory you own +## make install CLAW=on also install TRI CLAW; EARN=on, the lab node, is refused +## make uninstall remove exactly the files the install listed +## tri --version check an install, from PREFIX/bin +## make help this list; advisory shortcuts: make seal-check | seal | verify | warnings-baseline help: - @echo "t27 convenience targets:" - @echo " make seal-check Report whether the NMSE manifest seal is fresh" - @echo " vs sha256(bootstrap/src/compiler.rs) (advisory)." - @echo " make seal Recertify the NMSE seal against the current" - @echo " compiler.rs -- explicit, prompts for confirmation." - @echo " make install-hooks Install the local git hooks (incl. the advisory" - @echo " pre-push seal-staleness warning)." - @echo " make warnings-baseline Count non-test build warnings vs the recorded" - @echo " baseline and list the top files (advisory; #969)." - @echo " make verify Run all advisory pre-PR checks at once (seal-check" - @echo " + warnings-baseline + quick test + gate-preview" - @echo " + reseal-preview); never blocks. Skip flags:" - @echo " VERIFY_SKIP_TEST / VERIFY_SKIP_GATES /" - @echo " VERIFY_SKIP_RESEAL =1." - -# Advisory umbrella: run the seal-freshness, warnings-baseline, and a quick test -# back-to-back and print one compact summary. Never edits code, never reseals, -# always exits 0 -- a convenience entry point for a pre-PR glance. The real gate -# stays the required CI checks (gh api repos/gHashTag/t27/rules/branches/master). + @sed -n 's/^## / /p' $(MAKEFILE_LIST) + +install: + @case "$$(uname -s)-$$(uname -m)" in Darwin-arm64|Darwin-x86_64|Linux-aarch64|Linux-x86_64) ;; *) echo "unsupported platform: $$(uname -s) $$(uname -m)" >&2; exit 2;; esac + @case "$(PREFIX)" in *" "*|*..*) ;; /?*) exit 0;; esac; echo "PREFIX must be an absolute path, no spaces, no ..: $(PREFIX)" >&2; exit 2 + @test "$(EARN)" != on || { echo "EARN=on: no lab node ships in this checkout yet; nothing was installed" >&2; exit 2; } + ./scripts/tri disk + cargo build --release -p t27c -p tri $(if $(T27B),-p t27b) + @test -z "$(PIN)" || test "$$(target/release/t27c --version)" = "t27c $(PIN)" || { echo "version pin $(PIN) not met: $$(target/release/t27c --version)" >&2; exit 2; } + @set -e; for p in $(PAIRS); do s=$${p%%:*}; d=$(PREFIX)/$${p#*:}; mkdir -p "$$(dirname "$$d")"; m=755; case "$$s" in specs/*|.trinity/*) m=644;; esac; install -C -m $$m "$$s" "$$d"; done + @set -e; mkdir -p $(PREFIX)/share/t27; m=$$({ cat $(LIST) 2>/dev/null || :; printf '%s\n' $(PAIRS) | sed 's/^[^:]*://'; echo share/t27/install.list; } | sort -u); [ "$$m" = "$$(cat $(LIST) 2>/dev/null)" ] || printf '%s\n' "$$m" > $(LIST) + $(if $(filter on,$(CLAW)),@cd $(PREFIX)/share/t27/claw && $(PREFIX)/bin/t27c seal --verify specs/claw/service.t27) + +uninstall: + @if [ ! -f $(LIST) ]; then echo "nothing to remove: no $(LIST)"; elif grep -qE '^/|\.\.' $(LIST); then echo "$(LIST) names a path outside PREFIX; nothing removed" >&2; exit 2; else cd $(PREFIX) && xargs rm -f < share/t27/install.list && rmdir share/t27/claw/specs/claw share/t27/claw/specs share/t27/claw/.trinity/seals share/t27/claw/.trinity share/t27/claw share/t27 target/release target 2>/dev/null; true; fi + +# Advisory umbrella: seal freshness, warnings baseline and a quick test; always exits 0. verify: @scripts/verify.sh -# Advisory: report NMSE seal freshness. Exit 0 fresh / 2 stale / 3 unsealed. -# Never reseals; refreezing stays an explicit reviewed step. +# Advisory: NMSE seal freshness. Exit 0 fresh / 2 stale / 3 unsealed. Never reseals. seal-check: @scripts/reseal-check.sh -# Explicit, reviewed reseal: recertify the NMSE seal against the current -# compiler.rs. Prompts for confirmation (or RESEAL_YES=1); never automatic. +# Explicit, reviewed reseal of the NMSE seal; prompts, or RESEAL_YES=1. seal: @scripts/reseal-apply.sh -# Install the local git hooks (advisory pre-push includes the seal check). -install-hooks: - @scripts/install-git-hooks.sh - -# Advisory: count non-test build warnings vs the recorded baseline and list the -# top offending files, to track #969 dead-code progress. Never edits code. +# Advisory: non-test build warnings vs the recorded baseline (#969). Never edits code. warnings-baseline: @scripts/warnings-baseline.sh diff --git a/README.md b/README.md index 199cca9861..364992820b 100644 --- a/README.md +++ b/README.md @@ -235,15 +235,23 @@ t27 is the core of [Trinity S3AI](https://github.com/gHashTag/trinity) -- a neur ## Quick Start +T27 OS has one installer, and it is built with `make`. From a clone of this repository, with `cargo` on your PATH (macOS or Linux, arm64 or x86_64): + ```bash -# Clone -git clone https://github.com/gHashTag/t27.git -cd t27 +make install # build tri, t27c and t27b, install them under /usr/local +make -n install # print the plan, change nothing +make install PREFIX=$HOME/.local # install under a directory you own, no sudo +make install CLAW=on # also install TRI CLAW, the merge and gate agent of specs/claw/ +make uninstall # remove exactly the files the install listed +make help +tri --version # with PREFIX/bin on your PATH +``` -# Build the bootstrap compiler (Rust); use ./scripts/tri as the CLI entry (wraps t27c) -cd bootstrap && cargo build --release -cd .. +A second `make install` changes nothing, and t27b is installed on arm64 only. `CLAW=on` installs TRI CLAW's service spec (`specs/claw/service.t27`) with its seals and verifies them with `t27c seal --verify`; TRI CLAW has no server of its own, so that check is its start. The lab node that earns test-network XP by running lab gates is opt-in as `EARN=on`, off by default, and no lab node ships in this repository yet, so `make install EARN=on` stops before it changes anything. Phones do not run `make`; they join through the trios app. The decisions behind the installer are in [`specs/t27os/install.t27`](specs/t27os/install.t27). +### Use the toolchain + +```bash # Parse a spec (canonical CLI: tri → wraps bootstrap t27c) ./scripts/tri parse specs/base/types.t27 diff --git a/specs/t27os/install.t27 b/specs/t27os/install.t27 new file mode 100644 index 0000000000..d8c59a3ed6 --- /dev/null +++ b/specs/t27os/install.t27 @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/t27os/install.t27 -- how T27 OS is put on a machine: the one installer, `make install` (#11044, +; part of the T27 OS epic #11037 and the lab epic #11040). Owner, 2026-10-19: T27 OS has one installer, +; built with make, README.md says so, and TRI CLAW is part of T27 OS too. +; -- +; What is DECIDED here, and what carries it out: +; - Decided here: which platforms install, which binaries each one gets, what a prefix may be, which +; files an install lists, what a re-run changes, what a dry run changes, how a version pin is read, +; and the two opt-in flags. Both flags are off unless the user types exactly `on`. +; - Carried out by the Makefile at the repository root, which names the same files and the same flags. +; It only builds with cargo and copies with install(1); it decides nothing the list below does not. +; No generator turns a .t27 spec into a Makefile (t27c has no such backend), and a test cannot read the +; Makefile, so the agreement between the two is checked by reading `make -n install` and +; `make -n install CLAW=on` against the file lists here, not by a gate. That is the honest limit. +; -- +; Layout under PREFIX. The `tri` front door (scripts/tri) finds the compiler at +; /target/release/t27c, so the install gives it that shape: +; bin/tri the front door, a copy of scripts/tri +; bin/t27c the compiler +; bin/t27b the native compiler, only where it runs (arm64: it emits AArch64 and runs it +; in-process; specs/hosting/toolchain.t27 records the same rule for releases) +; target/release/tri the Rust `tri` binary (cli/tri) the front door hands its own subcommands to +; target/release/t27c the compiler again, where the front door looks for it +; share/t27/install.list every file any install under this PREFIX has listed, one per line +; With the TRI CLAW flag on, four more files go under share/t27/claw/: the service spec +; specs/claw/service.t27, the one spec it imports, specs/claw/text.t27, and their two seals. TRI CLAW has +; no server of its own: specs/claw/service.t27 says its execution is the consumer's own runner. So +; "start" for the installed copy means the check that runner makes: `t27c seal --verify` on the installed +; service spec, from the directory the four files form. +; -- +; The earn flag (the lab node that earns test-network XP by running lab gates) is decided here and +; refused here: LAB_NODE_SHIPS is false, because no lab node program is in this checkout. The job kind +; is specs/lab/job_kind.t27 (not on master at the time of writing) and nodes join through the trios app. +; plan_status answers PLAN_EARN_REFUSED, the Makefile stops before it changes anything, and the day a lab +; node lands, one constant and one more file list change here, with their tests. +; -- +; Counted strings, as in specs/claw/text.t27: a length rides along with every str a function reads, +; because `.len` and `==` on str do not lower under gen-c. Tests may use `.len`. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module T27osInstall; + +use claw::text; + +/// The platforms a source install supports, in this order. The names are the ones +/// specs/hosting/toolchain.t27 uses for the release assets, plus darwin-x64, which builds from source. +pub const PLATFORMS : [4]str = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64"]; +pub const PLATFORM_COUNT : u32 = 4; +pub const PLATFORM_NONE : u8 = 255; + +/// The prefix an install uses when the user names none. +pub const DEFAULT_PREFIX : str = "/usr/local"; + +/// A flag is on only when it is exactly this word. Anything else, "1", "yes", "ON", "true", is off. +pub const FLAG_ON : str = "on"; +pub const FLAG_OFF : str = "off"; + +/// No lab node program ships in this checkout; see the header. Flip this with the node, not before. +pub const LAB_NODE_SHIPS : bool = false; + +/// What an install lists. Core files go to every platform; t27b only where it runs; the TRI CLAW files +/// only when that flag is on; the manifest is last and lists itself. +pub const CORE_FILES : [4]str = ["bin/tri", "bin/t27c", "target/release/tri", "target/release/t27c"]; +pub const CORE_COUNT : u32 = 4; +pub const T27B_FILE : str = "bin/t27b"; +pub const CLAW_FILES : [4]str = [ + "share/t27/claw/specs/claw/service.t27", + "share/t27/claw/specs/claw/text.t27", + "share/t27/claw/.trinity/seals/claw_ClawService.json", + "share/t27/claw/.trinity/seals/claw_ClawText.json" +]; +pub const CLAW_COUNT : u32 = 4; +pub const MANIFEST : str = "share/t27/install.list"; + +/// A version pin is the compiler's own first line of `t27c --version`: this word, a space, then the +/// pinned version. An empty pin means the user pinned nothing. +pub const PIN_WORD : str = "t27c "; + +/// plan_status results. The Makefile checks in this order and stops at the first that is not OK. +pub const PLAN_OK : u8 = 0; +pub const PLAN_BAD_PLATFORM : u8 = 1; +pub const PLAN_BAD_PREFIX : u8 = 2; +pub const PLAN_EARN_REFUSED : u8 = 3; + +/// What one file does on an install. A file that is missing is copied; one that is already the same +/// is left alone, mtime included; one that differs is replaced. +pub const ACT_COPY : u8 = 0; +pub const ACT_KEEP : u8 = 1; +pub const ACT_REPLACE : u8 = 2; + +/// uname -s and uname -m to a platform index, or PLATFORM_NONE. Darwin reports arm64, Linux reports +/// aarch64; the other spelling of each is not a pair uname gives, so it is not accepted. +pub fn platform_index(os : str, os_len : usize, arch : str, arch_len : usize) -> u8 { + if (str_eq(os, os_len, "Darwin", 6)) { + if (str_eq(arch, arch_len, "arm64", 5)) { return 0; } + if (str_eq(arch, arch_len, "x86_64", 6)) { return 1; } + } + if (str_eq(os, os_len, "Linux", 5)) { + if (str_eq(arch, arch_len, "aarch64", 7)) { return 2; } + if (str_eq(arch, arch_len, "x86_64", 6)) { return 3; } + } + return PLATFORM_NONE; +} + +/// t27b emits AArch64 and runs it in-process, so it is installed on the two arm64 platforms only. +pub fn has_t27b(platform : u8) -> bool { + return platform == 0 or platform == 2; +} + +/// A prefix is an absolute path longer than "/" with no space and no ".." in it: the Makefile cannot +/// quote a space through every recipe line, and an install into the filesystem root is not a prefix. +pub fn prefix_ok(p : str, p_len : usize) -> bool { + if (p_len < 2) { return false; } + if (p[0] != '/') { return false; } + var i : usize = 0; + while (i < p_len) { + if (p[i] == ' ') { return false; } + if (i + 1 < p_len) { + if (p[i] == '.' and p[i + 1] == '.') { return false; } + } + i += 1; + } + return true; +} + +/// A flag is on only when it is exactly "on". +pub fn flag_on(v : str, v_len : usize) -> bool { + return str_eq(v, v_len, FLAG_ON, 2); +} + +/// Is this install allowed to start? First match wins: an unsupported platform, a bad prefix, then the +/// earn flag while no lab node ships. The TRI CLAW flag never refuses: its files are in this checkout. +pub fn plan_status(platform : u8, prefix_good : bool, earn : bool) -> u8 { + if (platform == PLATFORM_NONE) { return PLAN_BAD_PLATFORM; } + if (!prefix_good) { return PLAN_BAD_PREFIX; } + if (earn and !LAB_NODE_SHIPS) { return PLAN_EARN_REFUSED; } + return PLAN_OK; +} + +/// How many files an install lists, the manifest included. +pub fn file_count(t27b : bool, claw : bool) -> u32 { + var n : u32 = CORE_COUNT; + if (t27b) { n += 1; } + if (claw) { n += CLAW_COUNT; } + return n + 1; +} + +/// The i-th file an install lists, relative to the prefix; "" past the end. The uninstall list is this +/// same list: there is no second one. +pub fn file_at(i : u32, t27b : bool, claw : bool) -> str { + var k : u32 = i; + if (k < CORE_COUNT) { return CORE_FILES[k]; } + k -= CORE_COUNT; + if (t27b) { + if (k == 0) { return T27B_FILE; } + k -= 1; + } + if (claw) { + if (k < CLAW_COUNT) { return CLAW_FILES[k]; } + k -= CLAW_COUNT; + } + if (k == 0) { return MANIFEST; } + return ""; +} + +/// A listed file stays inside the prefix: relative, not empty, no "..". The uninstall removes the +/// manifest's lines relative to the prefix, so a line that escapes it is never written. +pub fn rel_ok(p : str, p_len : usize) -> bool { + if (p_len == 0) { return false; } + if (p[0] == '/') { return false; } + var i : usize = 0; + while (i + 1 < p_len) { + if (p[i] == '.' and p[i + 1] == '.') { return false; } + i += 1; + } + return true; +} + +/// What a file does, from two facts the copy can read: does it exist, and is it byte-identical. +pub fn step_action(exists : bool, same : bool) -> u8 { + if (!exists) { return ACT_COPY; } + if (same) { return ACT_KEEP; } + return ACT_REPLACE; +} + +/// Files a run changes: those that are not already the same. A second run over a finished install +/// has every file the same, so it changes nothing. +pub fn changes(total : u32, already_same : u32) -> u32 { + if (already_same >= total) { return 0; } + return total - already_same; +} + +/// A dry run lists the whole plan and changes nothing; a real run changes what `changes` says. +pub fn dry_changes(dry : bool, planned_changes : u32) -> u32 { + if (dry) { return 0; } + return planned_changes; +} + +/// The manifest only grows: a later install under the same prefix adds its files to the lines that +/// are already there, so an uninstall removes every file any install under it listed. old and added +/// are line counts, shared the lines both have. +pub fn manifest_lines(old : u32, added : u32, shared : u32) -> u32 { + return old + added - shared; +} + +/// Does the built compiler's first line say the pinned version? An empty pin accepts anything; a pin +/// is PIN_WORD then the version, nothing before it and nothing after it. +pub fn pin_ok(want : str, want_len : usize, have : str, have_len : usize) -> bool { + if (want_len == 0) { return true; } + if (have_len != 5 + want_len) { return false; } + if (!has_prefix(have, have_len, PIN_WORD, 5)) { return false; } + var i : usize = 0; + while (i < want_len) { + if (have[5 + i] != want[i]) { return false; } + i += 1; + } + return true; +} + +test platform_index_names_the_four_supported_pairs { + assert(platform_index("Darwin", 6, "arm64", 5) == 0); + assert(platform_index("Darwin", 6, "x86_64", 6) == 1); + assert(platform_index("Linux", 5, "aarch64", 7) == 2); + assert(platform_index("Linux", 5, "x86_64", 6) == 3); + assert(PLATFORM_COUNT == 4); + assert(PLATFORMS[0] == "darwin-arm64"); + assert(PLATFORMS[1] == "darwin-x64"); + assert(PLATFORMS[2] == "linux-arm64"); + assert(PLATFORMS[3] == "linux-x64"); +} + +test platform_index_refuses_every_other_pair { + assert(platform_index("Darwin", 6, "aarch64", 7) == PLATFORM_NONE); + assert(platform_index("Linux", 5, "arm64", 5) == PLATFORM_NONE); + assert(platform_index("Linux", 5, "i686", 4) == PLATFORM_NONE); + assert(platform_index("FreeBSD", 7, "x86_64", 6) == PLATFORM_NONE); + assert(platform_index("MINGW64_NT", 10, "x86_64", 6) == PLATFORM_NONE); + assert(platform_index("darwin", 6, "arm64", 5) == PLATFORM_NONE); + assert(platform_index("", 0, "", 0) == PLATFORM_NONE); +} + +test t27b_is_installed_only_on_the_arm64_platforms { + assert(has_t27b(0) == true); + assert(has_t27b(2) == true); + assert(has_t27b(1) == false); + assert(has_t27b(3) == false); + assert(has_t27b(PLATFORM_NONE) == false); +} + +test prefix_ok_takes_an_absolute_path_and_nothing_else { + assert(prefix_ok("/usr/local", 10) == true); + assert(prefix_ok("/tmp/t27-prefix", 15) == true); + assert(prefix_ok("/a", 2) == true); + assert(prefix_ok("", 0) == false); + assert(prefix_ok("/", 1) == false); + assert(prefix_ok("local", 5) == false); + assert(prefix_ok("./local", 7) == false); + assert(prefix_ok("/a b", 4) == false); + assert(prefix_ok("/a/../etc", 9) == false); + assert(prefix_ok("/a/.hidden", 10) == true); +} + +test the_default_prefix_is_itself_a_prefix { + const d : str = DEFAULT_PREFIX; + assert(prefix_ok(d, d.len) == true); +} + +test flags_are_off_unless_they_are_exactly_on { + assert(flag_on("on", 2) == true); + assert(flag_on(FLAG_OFF, 3) == false); + assert(flag_on("", 0) == false); + assert(flag_on("ON", 2) == false); + assert(flag_on("1", 1) == false); + assert(flag_on("yes", 3) == false); + assert(flag_on("true", 4) == false); + assert(flag_on("on ", 3) == false); + assert(flag_on("o", 1) == false); +} + +test the_earn_flag_is_refused_while_no_lab_node_ships { + assert(LAB_NODE_SHIPS == false); + assert(plan_status(3, true, true) == PLAN_EARN_REFUSED); + assert(plan_status(0, true, true) == PLAN_EARN_REFUSED); + assert(plan_status(3, true, false) == PLAN_OK); +} + +test plan_status_checks_platform_then_prefix_then_earn { + assert(plan_status(PLATFORM_NONE, false, true) == PLAN_BAD_PLATFORM); + assert(plan_status(PLATFORM_NONE, true, false) == PLAN_BAD_PLATFORM); + assert(plan_status(1, false, true) == PLAN_BAD_PREFIX); + assert(plan_status(1, false, false) == PLAN_BAD_PREFIX); + assert(plan_status(1, true, true) == PLAN_EARN_REFUSED); + assert(plan_status(1, true, false) == PLAN_OK); +} + +test the_default_plan_has_no_claw_file { + const arm : u32 = file_count(true, false); + const x64 : u32 = file_count(false, false); + assert(arm == 6); + assert(x64 == 5); + var i : u32 = 0; + while (i < arm) { + const f : str = file_at(i, true, false); + assert(f.len > 0); + var k : u32 = 0; + while (k < CLAW_COUNT) { + const c : str = CLAW_FILES[k]; + assert(str_eq(f, f.len, c, c.len) == false); + k += 1; + } + i += 1; + } + const past_arm : str = file_at(arm, true, false); + const past_x64 : str = file_at(x64, false, false); + assert(past_arm.len == 0); + assert(past_x64.len == 0); +} + +test the_claw_flag_adds_exactly_the_four_claw_files { + assert(file_count(true, true) == file_count(true, false) + CLAW_COUNT); + assert(file_count(false, true) == file_count(false, false) + CLAW_COUNT); + assert(file_at(5, true, true) == CLAW_FILES[0]); + assert(file_at(6, true, true) == CLAW_FILES[1]); + assert(file_at(7, true, true) == CLAW_FILES[2]); + assert(file_at(8, true, true) == CLAW_FILES[3]); + assert(file_at(4, false, true) == CLAW_FILES[0]); + assert(file_at(7, false, true) == CLAW_FILES[3]); + assert(CLAW_FILES[0] == "share/t27/claw/specs/claw/service.t27"); +} + +test the_listed_files_are_the_front_door_layout { + assert(file_at(0, true, false) == "bin/tri"); + assert(file_at(1, true, false) == "bin/t27c"); + assert(file_at(2, true, false) == "target/release/tri"); + assert(file_at(3, true, false) == "target/release/t27c"); + assert(file_at(4, true, false) == "bin/t27b"); + assert(file_at(4, false, false) == MANIFEST); + assert(file_at(5, true, false) == MANIFEST); + assert(file_at(9, true, true) == MANIFEST); + assert(MANIFEST == "share/t27/install.list"); +} + +test every_listed_file_stays_inside_the_prefix { + var t : u32 = 0; + while (t < 2) { + var c : u32 = 0; + while (c < 2) { + const n : u32 = file_count(t == 0, c == 0); + var i : u32 = 0; + while (i < n) { + const f : str = file_at(i, t == 0, c == 0); + assert(rel_ok(f, f.len) == true); + i += 1; + } + c += 1; + } + t += 1; + } + assert(rel_ok("", 0) == false); + assert(rel_ok("/etc/passwd", 11) == false); + assert(rel_ok("bin/../../etc", 13) == false); +} + +test a_second_run_changes_nothing { + assert(step_action(false, false) == ACT_COPY); + assert(step_action(true, true) == ACT_KEEP); + assert(step_action(true, false) == ACT_REPLACE); + assert(step_action(false, true) == ACT_COPY); + const total : u32 = file_count(true, true); + assert(changes(total, 0) == total); + assert(changes(total, total) == 0); + assert(changes(total, total - 1) == 1); +} + +test a_dry_run_lists_the_plan_and_changes_nothing { + const total : u32 = file_count(true, false); + assert(dry_changes(true, total) == 0); + assert(dry_changes(true, 0) == 0); + assert(dry_changes(false, total) == total); + assert(dry_changes(false, 0) == 0); +} + +test the_manifest_never_forgets_a_file { + const first : u32 = file_count(true, false); + const second : u32 = file_count(true, true); + assert(manifest_lines(0, first, 0) == first); + assert(manifest_lines(first, first, first) == first); + assert(manifest_lines(first, second, first) == second); + assert(manifest_lines(second, first, first) == second); +} + +test a_version_pin_is_the_compiler_first_line { + assert(pin_ok("", 0, "t27c 0.5.2", 10) == true); + assert(pin_ok("", 0, "anything", 8) == true); + assert(pin_ok("0.5.2", 5, "t27c 0.5.2", 10) == true); + assert(pin_ok("0.5.3", 5, "t27c 0.5.2", 10) == false); + assert(pin_ok("0.5.2", 5, "t27c 0.5.22", 11) == false); + assert(pin_ok("0.5.2", 5, "t27c 0.5", 8) == false); + assert(pin_ok("0.5.2", 5, "tri 0.5.2", 9) == false); + assert(pin_ok("0.5.2", 5, "", 0) == false); +}