diff --git a/.github/workflows/own-language.yml b/.github/workflows/own-language.yml index 0000b28a56..d8ad518fa7 100644 --- a/.github/workflows/own-language.yml +++ b/.github/workflows/own-language.yml @@ -2,8 +2,8 @@ name: Own Language # ONLY t27 (owner rule 2026-10-05, AGENTS.md 'Only t27'). # -# A pull request may add or modify .t27 specs and files t27c generated, and may -# delete anything else; hand-written code in any other language is denied. The +# A pull request may add or modify .t27 specs and files t27c generated, delete +# anything, and edit existing foreign files within the budget; a new one is denied. The # rule is specs/policy/own_language.t27, and the classifier that runs here is # its `t27c gen-c` output gen/c/policy/own_language.c -- never hand-edited (L2). # This file is plumbing: a one-line C main around the generated check_all(), @@ -12,9 +12,9 @@ name: Own Language # lefthook.yml (pre-commit and pre-push). # # The only override is the label `owner-approved-foreign`, which only the owner -# adds, and it waives the path check only, never the foreign-line budget. Changing the gate itself (the spec, its generated file, lefthook.yml, -# this workflow, tools/policy/foreign-exceptions.txt) needs that label too: the -# spec marks them protected. The label is applied only on the owner's explicit +# adds; it waives the path check only, never the foreign-line budget. Since 2026-10-18 it is needed only for a new foreign file and for the gate itself (the spec, its generated file, lefthook.yml, +# this workflow, tools/policy/foreign-exceptions.txt), which the +# spec marks protected. The label is applied only on the owner's explicit # approval, which the PR body quotes (AGENTS.md 'Only t27'). A base without the # exception list fails closed: rebase onto master. diff --git a/AGENTS.md b/AGENTS.md index e027bdfcf8..36995d46d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -133,18 +133,25 @@ another language; the hooks must stop it. It binds people, bees and agents alike - **Allowed:** `.t27` specs; files `t27c` generated under a generated root (`gen/`, `bootstrap/gen/`, `bootstrap/src/memory/generated/`); prose and data (`.md`, `.txt`, `.json`); and **deletions**. -- **Denied:** adding or modifying hand-written code in any other language -- +- **Denied:** adding a new hand-written file in any other language -- `.rs .py .ts .js .sh .zig .c .go .v .lean .yml .toml`, Dockerfile, Makefile and - the rest of the list in the spec. A change to existing Rust - (`bootstrap/src/compiler.rs`, `cli/t27b`) or Python is denied too. + the rest of the list in the spec. +- **Allowed without the label since 2026-10-18:** editing a foreign file that + already exists (`bootstrap/src/compiler.rs`, `cli/t27b`, a workflow `.yml`). + Owner rule, 2026-10-18 (translated): the label is not for every pull request, + only for the most important files. The edit is still held by `check_budget()`: + at most 40 added foreign lines per file and 80 per pull request, and never more + added than deleted. No label lifts that. - **The rule is** `specs/policy/own_language.t27`; its `t27c gen-c` output `gen/c/policy/own_language.c` runs in `lefthook.yml` (pre-commit, pre-push) and in `.github/workflows/own-language.yml` on every pull request. Read the spec for the exact list; do not copy it here. -- **The only override** is the label `owner-approved-foreign`. It is applied - only on the owner's explicit approval, and the pull request body quotes that - approval (in English, with its date and where it was given). Changing the gate - itself needs the label as well. +- **The only override** is the label `owner-approved-foreign`, and it is needed + only for the most important files: a new foreign file, and the gate itself + (the spec, its generated C, `lefthook.yml`, `.github/workflows/own-language.yml`, + `tools/policy/foreign-exceptions.txt`). It is applied only on the owner's + explicit approval, and the pull request body quotes that approval (in English, + with its date and where it was given). - **Owner-approved exceptions** live in `tools/policy/foreign-exceptions.txt`: data, one path prefix per line, each under a comment naming the approval. On an approved branch, add the entry in the same branch; the local hooks read the @@ -168,9 +175,10 @@ measure it are written in **t27**, not in Rust or Python by hand. file is never hand-edited (L2). - What gen cannot express yet is a defect of the self-host work (#5980): file it there. Do not work around it in a hand-written file. -- A hand-written addition or modification is denied by the gate in "Only t27" - above unless the owner labels the pull request `owner-approved-foreign`; such - a PR still names the spec that will replace it and links the port epic #6198. +- A new hand-written file is denied by the gate in "Only t27" above unless the + owner labels the pull request `owner-approved-foreign`; an edit to an existing + one passes within the budget. Either way the PR names the spec that will + replace it and links the port epic #6198. - The debt only shrinks, and the gate enforces it: since #8236 `check_budget()` refuses any pull request that adds more hand-written foreign lines than it deletes. On 2026-10-10 (master 1a7c8b939, `wc -l`) it is `cli/t27b/src/**/*.rs` 15627 lines and diff --git a/gen/c/policy/own_language.c b/gen/c/policy/own_language.c index 7b299800ef..9f478b8563 100644 --- a/gen/c/policy/own_language.c +++ b/gen/c/policy/own_language.c @@ -27,6 +27,7 @@ #define R_DATA 5 #define R_NOT_CODE 6 #define R_EXCEPTION 7 +#define R_MODIFIED 8 #define DENY_FROM 16 #define R_FOREIGN_EXT 16 #define R_FOREIGN_NAME 17 @@ -67,6 +68,7 @@ bool has_prefix(uint8_t* list, uint8_t* s, size_t from, size_t to); bool is_letter_of(uint8_t* list, uint8_t c); size_t base_start(uint8_t* s, size_t from, size_t to); size_t ext_dot(uint8_t* s, size_t from, size_t to); +uint8_t existing(uint8_t kind, uint8_t r); uint8_t reason(uint8_t kind, uint8_t* s, size_t from, size_t to); uint8_t verdict(uint8_t r); uint8_t classify(uint8_t kind, uint8_t* s, size_t from, size_t to); @@ -184,6 +186,13 @@ size_t ext_dot(uint8_t* s, size_t from, size_t to) { return d; } +uint8_t existing(uint8_t kind, uint8_t r) { + if ((kind == 'M')) { + return R_MODIFIED; + } + return r; +} + uint8_t reason(uint8_t kind, uint8_t* s, size_t from, size_t to) { if ((is_letter_of(KINDS, kind) == false)) { return R_MALFORMED; @@ -214,14 +223,14 @@ uint8_t reason(uint8_t kind, uint8_t* s, size_t from, size_t to) { return R_SPEC; } if (in_list(FOREIGN_NAMES, s, b, t, false)) { - return R_FOREIGN_NAME; + return existing(kind, R_FOREIGN_NAME); } if (has_prefix(FOREIGN_NAME_PREFIXES, s, b, t)) { - return R_FOREIGN_NAME; + return existing(kind, R_FOREIGN_NAME); } if ((d < t)) { if (in_list(FOREIGN_EXTS, s, (d + 1), t, true)) { - return R_FOREIGN_EXT; + return existing(kind, R_FOREIGN_EXT); } if (in_list(PROSE_EXTS, s, (d + 1), t, true)) { return R_PROSE; @@ -232,7 +241,7 @@ uint8_t reason(uint8_t kind, uint8_t* s, size_t from, size_t to) { return R_NOT_CODE; } if (has_prefix(SCRIPT_DIRS, s, f, t)) { - return R_SCRIPT_NO_EXT; + return existing(kind, R_SCRIPT_NO_EXT); } return R_NOT_CODE; } @@ -419,7 +428,7 @@ uint32_t check_all(uint8_t* buf, size_t n, uint8_t* out, size_t cap) { } bool counts_against_budget(uint8_t* s, size_t from, size_t to) { - uint8_t r = reason('M', s, from, to); + uint8_t r = reason('A', s, from, to); if ((((r == R_FOREIGN_EXT) || (r == R_FOREIGN_NAME)) || (r == R_SCRIPT_NO_EXT))) { return true; } @@ -540,6 +549,7 @@ uint32_t check_budget(uint8_t* buf, size_t n, uint8_t* out, size_t cap) { /* invariant every_allow_reason_is_below_every_deny_reason is not a C constant expression: (verdict(R_DATA) == ALLOW) */ /* invariant every_allow_reason_is_below_every_deny_reason is not a C constant expression: (verdict(R_NOT_CODE) == ALLOW) */ /* invariant every_allow_reason_is_below_every_deny_reason is not a C constant expression: (verdict(R_EXCEPTION) == ALLOW) */ +/* invariant every_allow_reason_is_below_every_deny_reason is not a C constant expression: (verdict(R_MODIFIED) == ALLOW) */ /* invariant every_allow_reason_is_below_every_deny_reason is not a C constant expression: (verdict(R_FOREIGN_EXT) == DENY) */ /* invariant every_allow_reason_is_below_every_deny_reason is not a C constant expression: (verdict(R_FOREIGN_NAME) == DENY) */ /* invariant every_allow_reason_is_below_every_deny_reason is not a C constant expression: (verdict(R_SCRIPT_NO_EXT) == DENY) */ @@ -562,9 +572,33 @@ void test_negative_control_hand_written_rust_is_denied(void) { assert((reason('A', "tools/new_tool.rs", 0, 17) == R_FOREIGN_EXT)); } -void test_modifying_t27b_rust_is_denied(void) { - assert((classify('M', "cli/t27b/src/lower.rs", 0, 21) == DENY)); - assert((classify('M', "bootstrap/src/compiler.rs", 0, 25) == DENY)); +void test_modifying_existing_foreign_code_needs_no_label(void) { + assert((classify('M', "cli/t27b/src/lower.rs", 0, 21) == ALLOW)); + assert((reason('M', "bootstrap/src/compiler.rs", 0, 25) == R_MODIFIED)); + assert((reason('M', "Dockerfile", 0, 10) == R_MODIFIED)); + assert((reason('M', "scripts/tri", 0, 11) == R_MODIFIED)); + assert((reason('M', ".github/workflows/cli-tri.yml", 0, 29) == R_MODIFIED)); +} + +void test_a_new_foreign_file_still_needs_the_label(void) { + assert((classify('A', "cli/t27b/src/lower.rs", 0, 21) == DENY)); + assert((reason('R', "cli/t27b/src/lower.rs", 0, 21) == R_FOREIGN_EXT)); + assert((reason('C', "scripts/tri", 0, 11) == R_SCRIPT_NO_EXT)); + assert((reason('T', "Dockerfile", 0, 10) == R_FOREIGN_NAME)); +} + +void test_the_most_important_files_still_need_the_label(void) { + assert((reason('M', "specs/policy/own_language.t27", 0, 29) == R_PROTECTED)); + assert((reason('M', ".github/workflows/own-language.yml", 0, 34) == R_PROTECTED)); + assert((judge('M', "tools/policy/foreign-exceptions.txt", 0, 35, 0, 0, true) == R_PROTECTED)); +} + +void test_a_modified_foreign_file_is_still_held_by_the_budget(void) { + uint8_t out[512] = {0}; + assert((counts_against_budget("cli/tri/src/gates.rs", 0, 20) == true)); + assert((counts_against_budget("Dockerfile", 0, 10) == true)); + assert((check_budget("1\t0\tcli/tri/src/main.rs\n", 24, &out, 512) == 1)); + assert((check_budget("0\t19\tcli/tri/src/gates.rs\n", 26, &out, 512) == 0)); } void test_generated_zig_is_allowed(void) { @@ -609,14 +643,14 @@ void test_prose_and_data_are_allowed(void) { void test_configuration_is_code(void) { assert((reason('A', "tools/x/lefthook.yml", 0, 20) == R_FOREIGN_EXT)); - assert((reason('M', ".github/workflows/ci.yaml", 0, 25) == R_FOREIGN_EXT)); - assert((reason('M', "bootstrap/Cargo.toml", 0, 20) == R_FOREIGN_EXT)); + assert((reason('A', ".github/workflows/ci.yaml", 0, 25) == R_FOREIGN_EXT)); + assert((reason('A', "bootstrap/Cargo.toml", 0, 20) == R_FOREIGN_EXT)); } void test_build_files_are_code_by_name(void) { assert((reason('A', "Dockerfile", 0, 10) == R_FOREIGN_NAME)); assert((reason('A', "deploy/Dockerfile.dev", 0, 21) == R_FOREIGN_NAME)); - assert((reason('M', "fpga/Makefile", 0, 13) == R_FOREIGN_NAME)); + assert((reason('A', "fpga/Makefile", 0, 13) == R_FOREIGN_NAME)); assert((reason('A', "x/CMakeLists.txt", 0, 16) == R_FOREIGN_NAME)); assert((reason('A', "tools/win.ps1", 0, 13) == R_FOREIGN_EXT)); } @@ -627,8 +661,8 @@ void test_extensions_fold_case(void) { } void test_an_extensionless_script_is_code(void) { - assert((reason('M', "scripts/tri", 0, 11) == R_SCRIPT_NO_EXT)); - assert((reason('M', ".githooks/pre-commit", 0, 20) == R_SCRIPT_NO_EXT)); + assert((reason('A', "scripts/tri", 0, 11) == R_SCRIPT_NO_EXT)); + assert((reason('A', ".githooks/pre-commit", 0, 20) == R_SCRIPT_NO_EXT)); assert((reason('M', "LICENSE", 0, 7) == R_NOT_CODE)); } @@ -676,9 +710,9 @@ void test_check_all_never_writes_past_cap(void) { } void test_an_owner_exception_allows_its_paths_only(void) { - assert((judge('M', "cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 34, 55, 0, 34, false) == R_EXCEPTION)); - assert((judge('M', "cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 34, 55, 0, 34, true) == R_EXCEPTION)); - assert((judge('M', "cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 34, 55, 0, 0, false) == R_FOREIGN_EXT)); + assert((judge('A', "cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 34, 55, 0, 34, false) == R_EXCEPTION)); + assert((judge('A', "cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 34, 55, 0, 34, true) == R_EXCEPTION)); + assert((judge('A', "cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 34, 55, 0, 0, false) == R_FOREIGN_EXT)); assert((excepted("cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 0, 34, 34, 55) == true)); } @@ -804,7 +838,10 @@ void test_check_budget_never_writes_past_cap(void) { #include int main(void) { test_negative_control_hand_written_rust_is_denied(); - test_modifying_t27b_rust_is_denied(); + test_modifying_existing_foreign_code_needs_no_label(); + test_a_new_foreign_file_still_needs_the_label(); + test_the_most_important_files_still_need_the_label(); + test_a_modified_foreign_file_is_still_held_by_the_budget(); test_generated_zig_is_allowed(); test_a_generated_root_is_a_prefix_not_a_substring(); test_deleting_python_is_allowed(); @@ -836,7 +873,7 @@ int main(void) { test_many_small_files_meet_the_total(); test_a_binary_file_adds_nothing_and_garbage_fails_closed(); test_check_budget_never_writes_past_cap(); - printf("All %d tests passed.\n", 33); + printf("All %d tests passed.\n", 36); return 0; } #endif /* T27_TEST_MAIN */ diff --git a/specs/policy/own_language.t27 b/specs/policy/own_language.t27 index d588c3f949..f94e276fa0 100644 --- a/specs/policy/own_language.t27 +++ b/specs/policy/own_language.t27 @@ -1,26 +1,32 @@ // SPDX-License-Identifier: Apache-2.0 ; specs/policy/own_language.t27 -- the "Only t27" gate: which changed paths a commit or a pull request may carry. -; Owner's hard rule, 2026-10-05: no new commit or PR adds or modifies hand-written code in any language -; other than t27. Allowed: .t27 specs, files t27c generated (under a generated root), and deletions -- so -; the foreign code already in the tree is debt that can only shrink. +; Owner's hard rule, 2026-10-05: no new commit or PR adds hand-written code in any language other than +; t27. Allowed: .t27 specs, files t27c generated (under a generated root), deletions, and edits to foreign +; files already in the tree that shrink them (2026-10-18) -- so the foreign code is debt that can only shrink. ; -- ; This file is the single source of truth for the rule. It is lowered by `t27c gen-c` to ; gen/c/policy/own_language.c (never hand-edited, L2); lefthook.yml (pre-commit, pre-push) and ; .github/workflows/own-language.yml feed `git diff --name-status` into check_all() below and fail on any -; DENY. The only override is the owner's label `owner-approved-foreign` on a pull request. +; DENY. The only override is the owner's label `owner-approved-foreign` on a pull request, needed only for a +; NEW foreign file and for the gate's own files (owner rule 2026-10-18: only the most important files). ; -- ; Decisions, stated where the rule lives: +; - Modifying a hand-written foreign file that already exists (git 'M') is ALLOW (R_MODIFIED), owner rule +; 2026-10-18: the label is for the most important files only, not for every pull request. Such an edit +; cannot grow the debt, because check_budget() below (shrink-only, 40 per file, 80 per diff) holds for +; every pull request and no label lifts it. A NEW foreign file (A, or R/C to a foreign path) stays DENY, +; and the gate's own files and the exception list stay PROTECTED: those still need the label. ; - Deletions are always ALLOW: removing foreign code is the only way the debt shrinks. ; - A rename or copy (R/C) is judged by its NEW path, like an addition: moving foo.py to bar.py writes bar.py. ; - Prose (.md .txt .rst .adoc) is ALLOW: the rule is about code, and a human reads prose. ; Exception: CMakeLists.txt is a build script wearing a .txt name, so it is listed by name and denied. ; - Data (.json .jsonl .hjson .csv .tsv .lock) is ALLOW: JSON written by tools (seals, conformance vectors, ; catalogs, lockfiles) is machinery, not a hand-written language. -; - .yml/.yaml/.toml are DENY: workflow and build configuration is code that runs. Cargo.toml counts. +; - .yml/.yaml/.toml are foreign: workflow and build configuration is code that runs. Cargo.toml counts. ; - An unknown extension (images, fonts, pdf, bitstreams) is ALLOW as not-code. The foreign list is the ; set of languages people write by hand; extend it here when a new one appears. ; - An extensionless file under a script directory (scripts/, bin/, .githooks/, .husky/) is a script -; with a shebang -- scripts/tri is bash -- so it is DENY. +; with a shebang -- scripts/tri is bash -- so it is foreign. ; - Generated roots are path prefixes: gen/ (every t27c backend), bootstrap/gen/ and ; bootstrap/src/memory/generated/ (t27c gen-rust output carried by the bootstrap crate). Conformance ; vectors are JSON and already ALLOW as data, so conformance/vectors/ is NOT a prefix: a prefix would also @@ -68,6 +74,7 @@ pub const R_PROSE : u8 = 4; pub const R_DATA : u8 = 5; pub const R_NOT_CODE : u8 = 6; pub const R_EXCEPTION : u8 = 7; +pub const R_MODIFIED : u8 = 8; pub const DENY_FROM : u8 = 16; pub const R_FOREIGN_EXT : u8 = 16; pub const R_FOREIGN_NAME : u8 = 17; @@ -195,6 +202,13 @@ pub fn ext_dot(s: []const u8, from: usize, to: usize) -> usize { return d; } +/// A foreign reason r for a file that already exists (git 'M') becomes R_MODIFIED: the budget bounds the +/// edit, and the label is kept for the gate's own files. Any other kind keeps r. +pub fn existing(kind: u8, r: u8) -> u8 { + if (kind == 'M') { return R_MODIFIED; } + return r; +} + /// The reason for one changed path s[from..to] with git status letter `kind`. A path git quoted /// ("...") is read without its quotes. pub fn reason(kind: u8, s: []const u8, from: usize, to: usize) -> u8 { @@ -210,15 +224,15 @@ pub fn reason(kind: u8, s: []const u8, from: usize, to: usize) -> u8 { const b : usize = base_start(s, f, t); const d : usize = ext_dot(s, f, t); if (d < t and in_list(OWN_EXTS, s, d + 1, t, true)) { return R_SPEC; } - if (in_list(FOREIGN_NAMES, s, b, t, false)) { return R_FOREIGN_NAME; } - if (has_prefix(FOREIGN_NAME_PREFIXES, s, b, t)) { return R_FOREIGN_NAME; } + if (in_list(FOREIGN_NAMES, s, b, t, false)) { return existing(kind, R_FOREIGN_NAME); } + if (has_prefix(FOREIGN_NAME_PREFIXES, s, b, t)) { return existing(kind, R_FOREIGN_NAME); } if (d < t) { - if (in_list(FOREIGN_EXTS, s, d + 1, t, true)) { return R_FOREIGN_EXT; } + if (in_list(FOREIGN_EXTS, s, d + 1, t, true)) { return existing(kind, R_FOREIGN_EXT); } if (in_list(PROSE_EXTS, s, d + 1, t, true)) { return R_PROSE; } if (in_list(DATA_EXTS, s, d + 1, t, true)) { return R_DATA; } return R_NOT_CODE; } - if (has_prefix(SCRIPT_DIRS, s, f, t)) { return R_SCRIPT_NO_EXT; } + if (has_prefix(SCRIPT_DIRS, s, f, t)) { return existing(kind, R_SCRIPT_NO_EXT); } return R_NOT_CODE; } @@ -393,7 +407,7 @@ pub const MSG_SHRINK : str = "all files: more hand-written foreign lines added t /// Does the path s[from..to] count against the budget? Every hand-written foreign kind does, and so do the /// gate's own foreign files; the gate's spec and its generated C do not. pub fn counts_against_budget(s: []const u8, from: usize, to: usize) -> bool { - const r : u8 = reason('M', s, from, to); + const r : u8 = reason('A', s, from, to); if (r == R_FOREIGN_EXT or r == R_FOREIGN_NAME or r == R_SCRIPT_NO_EXT) { return true; } if (r == R_PROTECTED) { if (has_prefix(GENERATED_PREFIXES, s, from, to)) { return false; } @@ -488,9 +502,33 @@ test negative_control_hand_written_rust_is_denied { assert reason('A', "tools/new_tool.rs", 0, 17) == R_FOREIGN_EXT; } -test modifying_t27b_rust_is_denied { - assert classify('M', "cli/t27b/src/lower.rs", 0, 21) == DENY; - assert classify('M', "bootstrap/src/compiler.rs", 0, 25) == DENY; +test modifying_existing_foreign_code_needs_no_label { + assert classify('M', "cli/t27b/src/lower.rs", 0, 21) == ALLOW; + assert reason('M', "bootstrap/src/compiler.rs", 0, 25) == R_MODIFIED; + assert reason('M', "Dockerfile", 0, 10) == R_MODIFIED; + assert reason('M', "scripts/tri", 0, 11) == R_MODIFIED; + assert reason('M', ".github/workflows/cli-tri.yml", 0, 29) == R_MODIFIED; +} + +test a_new_foreign_file_still_needs_the_label { + assert classify('A', "cli/t27b/src/lower.rs", 0, 21) == DENY; + assert reason('R', "cli/t27b/src/lower.rs", 0, 21) == R_FOREIGN_EXT; + assert reason('C', "scripts/tri", 0, 11) == R_SCRIPT_NO_EXT; + assert reason('T', "Dockerfile", 0, 10) == R_FOREIGN_NAME; +} + +test the_most_important_files_still_need_the_label { + assert reason('M', "specs/policy/own_language.t27", 0, 29) == R_PROTECTED; + assert reason('M', ".github/workflows/own-language.yml", 0, 34) == R_PROTECTED; + assert judge('M', "tools/policy/foreign-exceptions.txt", 0, 35, 0, 0, true) == R_PROTECTED; +} + +test a_modified_foreign_file_is_still_held_by_the_budget { + var out : [512]u8 = undefined; + assert counts_against_budget("cli/tri/src/gates.rs", 0, 20) == true; + assert counts_against_budget("Dockerfile", 0, 10) == true; + assert check_budget("1\t0\tcli/tri/src/main.rs\n", 24, &out, 512) == 1; + assert check_budget("0\t19\tcli/tri/src/gates.rs\n", 26, &out, 512) == 0; } test generated_zig_is_allowed { @@ -535,14 +573,14 @@ test prose_and_data_are_allowed { test configuration_is_code { assert reason('A', "tools/x/lefthook.yml", 0, 20) == R_FOREIGN_EXT; - assert reason('M', ".github/workflows/ci.yaml", 0, 25) == R_FOREIGN_EXT; - assert reason('M', "bootstrap/Cargo.toml", 0, 20) == R_FOREIGN_EXT; + assert reason('A', ".github/workflows/ci.yaml", 0, 25) == R_FOREIGN_EXT; + assert reason('A', "bootstrap/Cargo.toml", 0, 20) == R_FOREIGN_EXT; } test build_files_are_code_by_name { assert reason('A', "Dockerfile", 0, 10) == R_FOREIGN_NAME; assert reason('A', "deploy/Dockerfile.dev", 0, 21) == R_FOREIGN_NAME; - assert reason('M', "fpga/Makefile", 0, 13) == R_FOREIGN_NAME; + assert reason('A', "fpga/Makefile", 0, 13) == R_FOREIGN_NAME; assert reason('A', "x/CMakeLists.txt", 0, 16) == R_FOREIGN_NAME; assert reason('A', "tools/win.ps1", 0, 13) == R_FOREIGN_EXT; } @@ -553,8 +591,8 @@ test extensions_fold_case { } test an_extensionless_script_is_code { - assert reason('M', "scripts/tri", 0, 11) == R_SCRIPT_NO_EXT; - assert reason('M', ".githooks/pre-commit", 0, 20) == R_SCRIPT_NO_EXT; + assert reason('A', "scripts/tri", 0, 11) == R_SCRIPT_NO_EXT; + assert reason('A', ".githooks/pre-commit", 0, 20) == R_SCRIPT_NO_EXT; assert reason('M', "LICENSE", 0, 7) == R_NOT_CODE; } @@ -602,9 +640,9 @@ test check_all_never_writes_past_cap { } test an_owner_exception_allows_its_paths_only { - assert judge('M', "cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 34, 55, 0, 34, false) == R_EXCEPTION; - assert judge('M', "cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 34, 55, 0, 34, true) == R_EXCEPTION; - assert judge('M', "cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 34, 55, 0, 0, false) == R_FOREIGN_EXT; + assert judge('A', "cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 34, 55, 0, 34, false) == R_EXCEPTION; + assert judge('A', "cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 34, 55, 0, 34, true) == R_EXCEPTION; + assert judge('A', "cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 34, 55, 0, 0, false) == R_FOREIGN_EXT; assert excepted("cli/t27b/src/\n# owner 2026-10-05\n\ncli/t27b/src/lower.rs", 0, 34, 34, 55) == true; } @@ -729,6 +767,7 @@ invariant every_allow_reason_is_below_every_deny_reason { assert verdict(R_DATA) == ALLOW; assert verdict(R_NOT_CODE) == ALLOW; assert verdict(R_EXCEPTION) == ALLOW; + assert verdict(R_MODIFIED) == ALLOW; assert verdict(R_FOREIGN_EXT) == DENY; assert verdict(R_FOREIGN_NAME) == DENY; assert verdict(R_SCRIPT_NO_EXT) == DENY;