From b03d2bffc374d0ad9c13a4bbc700acd96fff0051 Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Wed, 11 Nov 2026 09:50:45 -0400 Subject: [PATCH 1/4] copyright: a short header is named as one, not as a missing notice engine#2175 / #2173 have been red on "Copyright notice on every file" for a day, and the message sends the reader the wrong way: the notice is on line 1. What those files keep is `// Copyright ...` and `// SPDX-License-Identifier: Apache-2.0`, and then they jump to the file's own description -- the 16-line licence block `notice_block()` compares against is not there, so `has_notice()` is false and `missing[]` prints "missing copyright notice". Measured on the two heads, same work, four lines apart: #2175 audio_mel.h line 4: // audio_mel.h -- the log-mel front-end: hann-windowed STFT ... #2133 audio_mel.h line 4: // Licensed under the Apache License, Version 2.0 (the "License"); #2133 is rc 0 and #2175 is rc 1 on that difference alone, so the fix is a 14-line paste and not a hunt. `short_notice()` names the shape: copyright + SPDX present but the licence block is not (paste the 16-line form): The exit code is unchanged -- a short header is still a failure, because the licence block is what the gate exists to require. Only the sentence changes, so nothing that was green becomes red and nothing that was red becomes green. Test: three arms in tests/copyright_check_untracked_test.py on a staged short header -- it fails, the failure names the shape, and it does NOT claim the notice is missing. Verified: that test 18 arms / 0 failures; `copyright.py --check` on this tree rc 0; and the predicate on the real files (short.h -> has_notice False / short_notice True, full.h -> has_notice True / short_notice False). Refs #2175, #2173, #2133. --- tests/copyright_check_untracked_test.py | 16 ++++++++++++++++ tools/copyright.py | 22 +++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/copyright_check_untracked_test.py b/tests/copyright_check_untracked_test.py index cbe0fd5c6..5237476af 100755 --- a/tests/copyright_check_untracked_test.py +++ b/tests/copyright_check_untracked_test.py @@ -95,6 +95,22 @@ def git(repo, *args): rc = run(repo, "--check") check(rc.returncode == 0, "and the check is green afterwards", rc.stdout + rc.stderr) + # engine#2175: a SHORT header (Copyright + SPDX, no licence block) is not a "missing copyright notice". + # The message has to name the shape, or a reader hunts for a notice that is on line 1 -- measured on + # #2175/#2173 whose audio headers kept only the two lines while #2133 carried the full block. + short = NOTICE.splitlines()[:2] + with open(os.path.join(repo, "short.h"), "w") as f: + f.write("\n".join(short) + "\n// short.h -- the file's own description\n") + git(repo, "add", "short.h") + rc = run(repo, "--check") + check(rc.returncode != 0, "a SHORT header still fails (the licence block is required)", rc.stdout) + check("copyright + SPDX present but the licence block is not" in rc.stdout, + "and the failure names the SHAPE, not a missing notice", rc.stdout + rc.stderr) + check("missing copyright notice: short.h" not in rc.stdout, + "and does NOT claim the notice is missing", rc.stdout + rc.stderr) + git(repo, "rm", "-q", "--cached", "short.h") + os.unlink(os.path.join(repo, "short.h")) + # an ignored file is not source: it must not need a notice. The .gitignore itself is source (the # repo's own carries the notice and is tracked), so it gets one here too. with open(os.path.join(repo, ".gitignore"), "w") as f: diff --git a/tools/copyright.py b/tools/copyright.py index 705dc082c..5dcebc625 100644 --- a/tools/copyright.py +++ b/tools/copyright.py @@ -164,6 +164,21 @@ def has_notice(text): return NOTICE[0] in head and NOTICE[1] in head and "Licensed under the Apache License, Version 2.0" in head +def short_notice(text): + """True when the header carries Copyright + SPDX but not the licence block. + + engine#2175: `notice_block()` returns the 16-line form for a known style, so a header that keeps only + `// Copyright ...` and `// SPDX-License-Identifier: Apache-2.0` and then jumps to the file's own + description reads as MISSING -- and the message says "missing copyright notice", which sends the reader + looking for a notice that is on line 1. Measured on engine#2175 / #2173 (their audio headers) against + #2133 (the same work, full block): copyright present, SPDX present, "Licensed under the Apache License" + absent. Naming the shape is the difference between a 15-line paste and a hunt. + """ + head = "\n".join(text.splitlines()[:NOTICE_WINDOW]) + return (NOTICE[0] in head and NOTICE[1] in head + and "Licensed under the Apache License, Version 2.0" not in head) + + def duplicated_notice(text): """True when the Copyright + SPDX pair appears twice inside the header window. @@ -329,7 +344,12 @@ def listing(*args): for path in unknown: print("no comment style known (add one to LINE_STYLES/BLOCK_STYLES or EXEMPT):", path) for path in missing: - print("missing copyright notice:", path) + # engine#2175: a short header is not a missing notice. Say which, so the fix is a paste rather than a + # search -- and keep the exit code the same (this is still a failure: the licence block is required). + if short_notice(open(path, encoding="utf-8", errors="replace").read()): + print("copyright + SPDX present but the licence block is not (paste the 16-line form):", path) + else: + print("missing copyright notice:", path) if missing or unknown or duplicated: print("run: python3 tools/copyright.py --fix", file=sys.stderr) return 1 From f3d8a3b1267119c8bd79ee10c5b4a181a97f5ff0 Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Wed, 11 Nov 2026 10:31:36 -0400 Subject: [PATCH 2/4] copyright: --fix replaces a short header instead of stacking a second one Measured on the shape #2199 names: a header keeping only `// Copyright ...` and `// SPDX-License-Identifier: Apache-2.0`. $ copyright.py --check copyright + SPDX present but the licence block is not: short.h $ copyright.py --fix added: short.h $ copyright.py --check copyright notice duplicated inside the header window: short.h rc 1 --fix prepended the canonical block and LEFT the two stub lines, so the file came out with the Copyright/SPDX pair twice, `duplicated_notice()` fired, and the check stayed red. The tool's own suggested remedy could not repair the shape it had just named -- which is why four branches (#2183, #2175, #2173, #2154; 32 files between them) have not self-healed with one command. `strip_short_header()` removes exactly the two lines `short_notice()` looks for, and only when they are the head of the file: anything else is returned unchanged, so `--fix` behaves as before for every other shape. The file's own header text and a leading `#!` are kept. After: --fix -> "added: short.h"; --check rc 0; one SPDX line; the block present; the description line intact. Test: five arms in tests/copyright_check_untracked_test.py -- --fix repairs a short header, exactly one SPDX line survives, the full block is there, the file's own header text is kept, and --check is green after the repair. That file: 23 arms, 0 failures. `copyright.py --check` on this tree rc 0 (nothing that was green became red). Refs #2199, #2175, #2183, #2154. --- tests/copyright_check_untracked_test.py | 15 ++++++++++++++- tools/copyright.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/copyright_check_untracked_test.py b/tests/copyright_check_untracked_test.py index 5237476af..c32f4efc6 100755 --- a/tests/copyright_check_untracked_test.py +++ b/tests/copyright_check_untracked_test.py @@ -108,8 +108,21 @@ def git(repo, *args): "and the failure names the SHAPE, not a missing notice", rc.stdout + rc.stderr) check("missing copyright notice: short.h" not in rc.stdout, "and does NOT claim the notice is missing", rc.stdout + rc.stderr) - git(repo, "rm", "-q", "--cached", "short.h") + # engine#2175: --fix must REPLACE the stub, not stack the block above it. Before this it left both, so the + # file came out with the Copyright/SPDX pair twice, the duplicate rule fired, and --check stayed red -- the + # tool's own remedy could not fix the shape it had just named. + rc = run(repo, "--fix") + check(rc.returncode == 0, "--fix repairs a short header", rc.stdout + rc.stderr) + got = open(os.path.join(repo, "short.h")).read() + check(got.count("SPDX-License-Identifier") == 1, + "and exactly one SPDX line survives the repair", f"{got.count('SPDX-License-Identifier')} found") + check("Licensed under the Apache License, Version 2.0" in got, + "and the full licence block is there", got[:200]) + check("the file's own description" in got, "and the file's own header text is kept", got) + rc = run(repo, "--check") + check(rc.returncode == 0, "and --check is green after the repair", rc.stdout + rc.stderr) os.unlink(os.path.join(repo, "short.h")) + git(repo, "add", "-A", "short.h") # an ignored file is not source: it must not need a notice. The .gitignore itself is source (the # repo's own carries the notice and is tracked), so it gets one here too. diff --git a/tools/copyright.py b/tools/copyright.py index 5dcebc625..a286a4b08 100644 --- a/tools/copyright.py +++ b/tools/copyright.py @@ -266,7 +266,27 @@ def repair_notice(text, block): return repaired if has_notice(repaired) else add_notice(repaired, block) +def strip_short_header(text): + """Drop a short header (Copyright + SPDX and nothing after it) so add_notice can put the block there. + + engine#2175: `--fix` on a short header prepended the canonical block and LEFT the two stub lines, so the + file came out with the Copyright/SPDX pair twice, `duplicated_notice()` fired, and `--check` was still + red -- the tool's own suggested remedy could not fix the shape it told four branches about. The stub is + exactly the two lines `short_notice()` looks for; if the file's head does not match that shape, this + returns the text unchanged and the caller's behaviour is what it was. + """ + lines = text.splitlines(keepends=True) + if len(lines) >= 2 and NOTICE[0] in lines[0] and NOTICE[1] in lines[1]: + rest = lines[2:] + # keep a `#!` line in front of the header if there is one + if rest and rest[0].strip() == "" and len(rest) > 1: + rest = rest[1:] + return "".join(rest) + return text + + def add_notice(text, block): + text = strip_short_header(text) lines = text.splitlines(keepends=True) insert_at = 1 if lines and lines[0].startswith("#!") else 0 # A near-miss of this repo's holder is REPLACED, not stacked beside: `--fix` used to insert the canonical From 380c4c38fb97469870dbbfa7134c36aaba9391ad Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Wed, 11 Nov 2026 11:20:34 -0400 Subject: [PATCH 3/4] copyright: --fix finds a short header behind a shebang The first version of strip_short_header() read lines 0-1 only, so a script's short header -- which sits on lines 1-2, behind `#!` -- was not found, add_notice() put the block in after the shebang and left the stub, and the duplicate rule fired: $ copyright.py --check copyright + SPDX present but the licence block is not: short.sh $ copyright.py --fix added: short.sh $ copyright.py --check copyright notice duplicated inside the header window: short.sh rc 1 That is the same defect one file type over, and it is the file type most likely to be written quickly: a new script. Now the two lines are located after a leading `#!` and the shebang is carried through. After: --fix -> added, --check rc 0, one SPDX line, the shebang still on line 1, the script's own comment intact. Five arms in tests/copyright_check_untracked_test.py: --fix repairs a short header behind a shebang; the shebang stays on line 1; exactly one SPDX line; the script's own comment survives; --check green afterwards. That file: 28 arms, 0 failures. `copyright.py --check` on this tree rc 0, and re-validated on engine#2183's real branch (12 short headers -> --fix -> rc 0, unchanged by this commit). Refs #2199, #2183. --- tests/copyright_check_untracked_test.py | 19 +++++++++++++++++++ tools/copyright.py | 10 ++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/copyright_check_untracked_test.py b/tests/copyright_check_untracked_test.py index c32f4efc6..3a2ae7e56 100755 --- a/tests/copyright_check_untracked_test.py +++ b/tests/copyright_check_untracked_test.py @@ -121,6 +121,25 @@ def git(repo, *args): check("the file's own description" in got, "and the file's own header text is kept", got) rc = run(repo, "--check") check(rc.returncode == 0, "and --check is green after the repair", rc.stdout + rc.stderr) + # engine#2175 review: a script's short header sits BEHIND its `#!`. The first version of strip_short_header() + # looked at line 1 only, so the stub was not found, the block went in after the shebang, and the file ended up + # with two notices -- the same defect one file type over. + with open(os.path.join(repo, "short.sh"), "w") as f: + f.write("#!/usr/bin/env bash\n" + NOTICE.splitlines()[0] + "\n" + NOTICE.splitlines()[1] + + "\n# short.sh -- a script\necho hi\n") + git(repo, "add", "short.sh") + rc = run(repo, "--fix") + check(rc.returncode == 0, "--fix repairs a short header behind a shebang", rc.stdout + rc.stderr) + got = open(os.path.join(repo, "short.sh")).read() + check(got.startswith("#!/usr/bin/env bash\n"), + "the shebang stays on line 1", got.splitlines()[0]) + check(got.count("SPDX-License-Identifier") == 1, + "and exactly one SPDX line in the script too", f"{got.count('SPDX-License-Identifier')} found") + check("short.sh -- a script" in got, "and the script's own comment survives", got) + rc = run(repo, "--check") + check(rc.returncode == 0, "and --check is green for the script", rc.stdout + rc.stderr) + os.unlink(os.path.join(repo, "short.sh")) + git(repo, "add", "-A", "short.sh") os.unlink(os.path.join(repo, "short.h")) git(repo, "add", "-A", "short.h") diff --git a/tools/copyright.py b/tools/copyright.py index a286a4b08..e7b51ce51 100644 --- a/tools/copyright.py +++ b/tools/copyright.py @@ -276,12 +276,14 @@ def strip_short_header(text): returns the text unchanged and the caller's behaviour is what it was. """ lines = text.splitlines(keepends=True) - if len(lines) >= 2 and NOTICE[0] in lines[0] and NOTICE[1] in lines[1]: - rest = lines[2:] - # keep a `#!` line in front of the header if there is one + # A script's short header starts on line 2, behind its `#!`: without this, the stub is not found, the block + # is prepended after the shebang, and the file ends up with two notices again (measured: short.sh). + at = 1 if lines and lines[0].startswith("#!") else 0 + if len(lines) >= at + 2 and NOTICE[0] in lines[at] and NOTICE[1] in lines[at + 1]: + head, rest = lines[:at], lines[at + 2:] if rest and rest[0].strip() == "" and len(rest) > 1: rest = rest[1:] - return "".join(rest) + return "".join(head + rest) return text From 7dd6e43a76ad7e3b8ddcde9836f325b701d8bbd6 Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Thu, 12 Nov 2026 04:01:03 -0400 Subject: [PATCH 4/4] copyright: drop strip_short_header, now that main's --fix does that job Answers the CHANGES at 751aa35. Main has taken two --fix changes since this branch was cut: - #2192 (83012c89) repairs a short header, including one behind a `#!` -- `add_notice` filters `canonical_notice_line()` out of the head before prepending the block; - #2228 (ada88bb5) makes --fix keep blank lines. Merged on top of those, this branch's `strip_short_header()` was redundant and actively wrong: its `if rest and rest[0].strip() == "" and len(rest) > 1: rest = rest[1:]` dropped a blank line after the stub, undoing #2228. The reviewer measured both: | file | main | this head (before) | |---|---|---| | blank.sh (`#!` + stub + blank + code) | blank kept | blank dropped | | blank.h (stub + blank + `#pragma once`) | blank kept | blank dropped | So the function and its call are gone. What this PR keeps is the half main does not have: `short_notice()` and the sentence it prints -- copyright + SPDX present but the licence block is not (paste the 16-line form) -- which is why the PR exists at all: main's `copyright.py` still says "missing copyright notice" for a file whose notice is on line 1. Re-ran the reviewer's four-case table at this head: `short.h`, `tool.py` (#! + # stub), `blank.sh` (#! + stub + blank), `blank.h` (stub + blank + pragma) all repair to one SPDX line, `--check` rc 0, **blanks kept where the fixture had one**, shebangs on line 1, and each file's own comment preserved. tests/copyright_check_untracked_test.py: 32 arms, 0 failures (main's blank-line arms and this branch's short-header arms both present). copyright.py --check on this tree: rc 0. --- tools/copyright.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/tools/copyright.py b/tools/copyright.py index dff24b4a6..95bfff724 100755 --- a/tools/copyright.py +++ b/tools/copyright.py @@ -304,29 +304,7 @@ def repair_notice(text, block): return repaired if has_notice(repaired) else add_notice(repaired, block) -def strip_short_header(text): - """Drop a short header (Copyright + SPDX and nothing after it) so add_notice can put the block there. - - engine#2175: `--fix` on a short header prepended the canonical block and LEFT the two stub lines, so the - file came out with the Copyright/SPDX pair twice, `duplicated_notice()` fired, and `--check` was still - red -- the tool's own suggested remedy could not fix the shape it told four branches about. The stub is - exactly the two lines `short_notice()` looks for; if the file's head does not match that shape, this - returns the text unchanged and the caller's behaviour is what it was. - """ - lines = text.splitlines(keepends=True) - # A script's short header starts on line 2, behind its `#!`: without this, the stub is not found, the block - # is prepended after the shebang, and the file ends up with two notices again (measured: short.sh). - at = 1 if lines and lines[0].startswith("#!") else 0 - if len(lines) >= at + 2 and NOTICE[0] in lines[at] and NOTICE[1] in lines[at + 1]: - head, rest = lines[:at], lines[at + 2:] - if rest and rest[0].strip() == "" and len(rest) > 1: - rest = rest[1:] - return "".join(head + rest) - return text - - def add_notice(text, block): - text = strip_short_header(text) lines = text.splitlines(keepends=True) insert_at = 1 if lines and lines[0].startswith("#!") else 0 # A near-miss of this repo's holder is REPLACED, not stacked beside: `--fix` used to insert the canonical