Skip to content

chore(license): correct two Apache-2.0 headers and the LICENSE holder [skip-runtime-e2e] - #241

Merged
saurabhjain1592 merged 1 commit into
mainfrom
chore/license-headers-mit
Sep 3, 2026
Merged

chore(license): correct two Apache-2.0 headers and the LICENSE holder [skip-runtime-e2e]#241
saurabhjain1592 merged 1 commit into
mainfrom
chore/license-headers-mit

Conversation

@saurabhjain1592

@saurabhjain1592 saurabhjain1592 commented Sep 3, 2026

Copy link
Copy Markdown
Member

Closes #238.

LICENSE has read MIT License since the initial commit (a424b6a, 2025-12-04). Two files declared Apache-2.0, and the holder line read getaxonflow where every sibling SDK reads AxonFlow. Neither header was ever a relicence question: both files were MIT all along and the headers were wrong statements about them, so no contributor-consent question arises.

Census, run by every form rather than the one the issue named

The sibling axonflow-sdk-go issue undercounted by 4.25x because the cross-repo report behind it searched only for SPDX-License-Identifier and missed thirteen Apache prose blocks. So this repo was re-measured across every form first:

$ git grep -n -iE 'SPDX-License-Identifier|Licensed under the|License, Version|Business Source|BUSL|Apache License|GNU General|Mozilla Public' origin/main
  CONTRIBUTING.md:291: ... will be licensed under the MIT License.     <- correct, unchanged
  axonflow/hitl.py:2:# SPDX-License-Identifier: Apache-2.0
  tests/test_hitl.py:2:# SPDX-License-Identifier: Apache-2.0

Two, exactly as #238 says. No prose-block form exists here.

Authorship census

$ git log --all --format='%ae' | sort | uniq -c | sort -rn
 235 saurabhjain1592@gmail.com
  20 saurabh.jain@getaxonflow.com
   7 thegzak@gmail.com
   4 gzak@getaxonflow.com

Two operator addresses and two belonging to gzak - the person the operator's answer already covers. No unknown third party. Moot regardless, since LICENSE has always been MIT.

What changed

Two identifier lines, and the holder name:

 # Copyright 2026 AxonFlow
-# SPDX-License-Identifier: Apache-2.0
+# SPDX-License-Identifier: MIT
-Copyright (c) 2025 getaxonflow
+Copyright (c) 2025 AxonFlow

All five SDKs now name the same holder, each keeping its own first year - Rust's 2026 is correct for Rust:

repo holder line
sdk-go Copyright (c) 2025 AxonFlow
sdk-typescript Copyright (c) 2025 AxonFlow
sdk-java Copyright (c) 2025 AxonFlow
sdk-rust Copyright (c) 2026 AxonFlow
sdk-python Copyright (c) 2025 AxonFlow

The guard

tests/test_license_metadata.py, 12 tests, under tests/ so pytest runs it with no CI wiring.

Three artifacts state the licence here and different consumers read different ones: GitHub reads LICENSE; PyPI publishes pyproject.toml's license field; and a pip-licenses-style audit reads the trove classifier. All three are asserted, and each has its own mutant, because they can disagree.

Three rules are positional rather than textual, and they are the reason this port is not just two line edits. A licence sweep rewrites the top of a file, which is exactly where Python keeps meaning in position:

  • a shebang is only a shebang on line 1;
  • a PEP 263 coding cookie is honoured only on line 1, or line 2 when line 1 is a shebang - the rule is positional and conditional, so "near the top" is not assertable;
  • # !/usr/bin/env python is inert and invisible to the position rule above, since that keys on the literal #! - the two failures hide each other.

A header inserted above a cookie pushes it to line 3, Python ignores it, and the file silently decodes as UTF-8. Nothing fails at that moment; the damage appears later in whichever non-ASCII literal happens to matter.

This is not hypothetical. The sibling Go port shipped the equivalent defect into review: the sweep re-spaced //go:build to // go:build, disabling a build constraint - and go build, go vet and the entire test suite passed with it disabled, because a file that merely starts compiling breaks nothing anyone asserts. Only the formatter noticed. These three rules make the Python equivalent impossible to ship unnoticed, rather than depending on someone remembering to run a formatter.

Scope stated precisely. The walk skips dependency and build trees wholesale, so the copyright rule covers this repository's own source and says so. An earlier version of the Go sibling's comment claimed the guard had "no exemption list at all" and that a vendored file would "force the decision" - both false, and R3 proved it with a real vendor tree (17 files, 7 Apache-prose, 15 third-party notices) that passed every licence rule. A guard narrower than its own comment is worse than a narrow guard, because the comment is what the next person relies on. Dropping the exclusion was measured and rejected: it would fail the guard on correct code the first time anyone vendored, which is how a guard gets deleted rather than fixed.

Mutation proof

Each row carries a pytest count, so no row can pass because nothing ran.

=== control ===
  control / spdx identifier rule                    expect=PASS got=PASS [1 passed] OK
  control / prose rule                              expect=PASS got=PASS [1 passed] OK
=== M1: one file's SPDX identifier back to Apache-2.0 ===
  M1 / spdx identifier rule                         expect=FAIL got=FAIL [1 failed] OK
  M1 / prose rule (blind to a bare tag)             expect=PASS got=PASS [1 passed] OK
=== M2: an Apache PROSE block appears (sdk-go had 13) ===
  M2 / prose rule                                   expect=FAIL got=FAIL [1 failed] OK
  M2 / spdx identifier rule (blind to prose)        expect=PASS got=PASS [1 passed] OK
=== M3: LICENSE back to Apache text ===
  M3 / license_file_is_mit                          expect=FAIL got=FAIL [1 failed] OK
=== M3b: LICENSE holder reverted to 'getaxonflow' ===
  M3b / license_file_is_mit (holder name)           expect=FAIL got=FAIL [1 failed] OK
=== M4: pyproject license field back to Apache-2.0 ===
  M4 / pyproject_declares_mit                       expect=FAIL got=FAIL [1 failed] OK
=== M4b: the MIT trove CLASSIFIER removed, field left correct ===
  M4b / pyproject_declares_mit (classifier)         expect=FAIL got=FAIL [1 failed] OK
=== M5: revert the tag scan to case-SENSITIVE ===
  M5 / identifier reader                            expect=FAIL got=FAIL [1 failed] OK
=== M6: a shebang pushed off line 1 ===
  M6 / shebangs_stay_on_line_one                    expect=FAIL got=FAIL [1 failed] OK
=== M7: a coding cookie pushed to line 3 by an inserted header ===
  M7 / coding_cookies_stay_where_python_looks       expect=FAIL got=FAIL [1 failed] OK
=== M8: a vendored third-party copyright notice appears ===
  M8 / every copyright notice is AxonFlow's         expect=FAIL got=FAIL [1 failed] OK

The shebang rules are proven across both broken orderings plus a negative control, because the two failures conceal each other: a re-spaced # ! is inert to the kernel and invisible to the position rule, which keys on the literal #!.

  (a) re-spaced in place, line 1        -> exit=1  (want non-zero)
  (b) re-spaced AND pushed to line 3    -> exit=1  (want non-zero)
  (c) correct form, but moved to line 2 -> exit=1  (want non-zero)
  (d) CONTROL: correct and on line 1    -> exit=0  (want 0)
      clean tree, full guard            -> exit=0  (want 0)

(b) is the ordering no other rule here can see, and (d) is what stops the rules being satisfied by simply rejecting everything.

The two expect=PASS rows under M1 and M2 are the informative ones: each licence rule is blind to exactly what the other catches. This repo has only the tag form today; Go had thirteen of the other, so shipping one rule would have been the difference between catching and missing that class.

Verification

check result
ruff format --check . (ruff 0.16.6) clean, 140 files; the origin/main baseline was recorded with the same version before any edit
ruff check . (ruff 0.16.6) All checks passed
pytest 1378 passed, 30 skipped
the guard 12/12
the shebang / cookie rules all four orderings proven, including a negative control
non-MIT declarations remaining 0 (was 2)
no behaviour change both modified .py files are byte-identical to origin/main once the leading comment run is removed - 0 differ

Every formatter figure above is from ruff 0.16.6, and the version is stated because it changes the answer. CI pins ruff>=0.16,<0.17. The ruff on my PATH was 0.15.8, and the two disagree materially on this repository. Measured on ONE tree at a time, because my first two attempts at this figure compared readings taken from different trees and so were not a version comparison at all:

same tree (this branch): 0.15.8 -> 125 formatted   0.16.5 -> 140 formatted   gap 15
same tree (origin/main): 0.15.8 -> 124 formatted   0.16.5 -> 139 formatted   gap 15

The version gap is 15 files, on either tree. The one-file difference between the trees is this PR's own added guard. Quoting "124 against 140" folded both differences into a single number and overstated the gap by one. My first baseline was therefore evidence about a different tool than the one CI runs, and would have been quoted as though it were not.

The pinned version needs no special setup, because it is already in this repo's dev extras (pyproject.toml:51):

$ python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
$ .venv/bin/ruff --version
ruff 0.16.6

Cross-checked against an independently installed 0.16.5: both report 140 files formatted, so the figure is stable across the pinned range rather than specific to one patch release. This is the same family as a green PR board being evidence about the merge ref rather than the branch tip — the measurement is real, it is just about something other than what you meant to measure.

Skip-runtime-e2e justification

Licence metadata only, no behaviour. There is no user-facing surface to demonstrate because there is no runtime change to demonstrate through. Both modified .py files are byte-identical to origin/main once the leading comment run is removed — 0 differ; the other changes are the LICENSE holder line and the guard test.

A runtime-e2e leg would boot a stack, exercise a path this PR provably did not touch, and pass. The properties asserted here are about files, and tests/test_license_metadata.py pins them under ten mutants, a five-case shebang matrix, and a coding-cookie rule derived from tokenize.detect_encoding rather than from a reading of PEP 263.

definition-of-done.yml drops edited from its triggers, so this section is inert until the title carries the marker and a new push re-fires the gate.

Not merged by me

Worker does not merge. Master runs the independent R3 and merges. No ci: tier labels exist in this repository, so there is none to apply.

@saurabhjain1592
saurabhjain1592 force-pushed the chore/license-headers-mit branch from a2414a8 to b58438b Compare September 3, 2026 17:11
@saurabhjain1592
saurabhjain1592 marked this pull request as ready for review September 3, 2026 17:11
@saurabhjain1592

Copy link
Copy Markdown
Member Author

R3, joint review of the three licence-guard ports - FIXES-REQUIRED on all three

Reviewed as a set, because the same guard in three idioms should differ only where the language forces it. Heads: sdk-go#222 at 4ab6a9ff, sdk-typescript#272 at 0814dc12, sdk-python#241 at b58438be. Every mutant and plant restored; all three clones clean.

Verified

Census reproduced with working positive controls, main to head: Go 4 BUSL tags plus 13 Apache prose blocks of 119 Go files, to zero, 18 MIT; TypeScript 2 to zero; Python 2 to zero plus the copyright-holder fix. Content identity holds in all three: stripping only the leading comment run, every modified file is byte-identical to main, with the strip proven non-vacuous (12, 11 and 3 lines removed on samples). Only Python's LICENSE differs, intentionally. Every claimed mutant re-applied and reproduced: Go M1 to M6, TypeScript M1 to M7, Python M1 to M8. Toolchains: Go gofmt -l empty with go vet and go vet -tags=integration clean; TypeScript Prettier, ESLint and build clean; Python ruff 0.16.6 resolved from the repo's own pin reporting 140 files, ruff check clean, pytest 1378 passed and 30 skipped.

High

H1 - TypeScript has no re-spaced-shebang rule, so two of the four claimed matrix cases do not hold. tests/license-metadata.test.ts:329 keys on line.startsWith('#!'), which # ! cannot match. On scripts/postinstall.js:

Case Claimed Measured
re-spaced in place, line 1 exit 1 26 passed, 26 total, exit 0
re-spaced and pushed to line 3 exit 1 26 passed, 26 total, exit 0
correct form moved to line 2 exit 1 ✕ a shebang stays on line 1, exit 1
control, correct on line 1 exit 0 exit 0

Python holds all four. The claim that both carry the matrix was reported to master before it was true, and this is the class the train keeps paying for: a report that describes the intent rather than the tree. It is drift rather than language-forced, and the Python rule (re.match(r"^#[ \t]+!", line) over lines[:5], test_license_metadata.py:314) ports verbatim. TypeScript has 16 shebang-bearing files.

H2 - the Go and TypeScript PR bodies still assert the falsehood round 1 rejected. Both guard files were corrected (license_metadata_test.go:270-283 and the TypeScript equivalent at :237-248 explicitly retract it), but both bodies still read that the guard carries "no exemption list at all" and that a vendored arrival "fails and forces the decision". Disproved: node_modules/other/x.js carrying Apache prose, an Apache tag and Copyright 2019 The Other Company passes; the identical content at a scanned path fires three rules. Python's body states it correctly. The body is the artifact a reviewer reads, so the retraction has to land there too.

Medium

M1 - the Python PEP 263 rule is narrower than PEP 263, in the false-positive direction. test_license_metadata.py:336 allows line 2 only when line 1 is a shebang, but CPython consults line 2 whenever line 1 is blank or a comment. Planting # Copyright 2026 AxonFlow then # -*- coding: latin-1 -*- gives the guard 1 failed while tokenize.detect_encoding returns iso-8859-1: Python honours what the guard rejects. These repos put a copyright comment on line 1, so this is the live shape, and a guard that fails on correct code is one that gets deleted rather than fixed. Fix: allow line 2 when line 1 matches ^[ \t\f]*(?:#|$). The cookie regex itself is correct, PEP 263's real coding[:=] form, matching # coding=latin-1 and a vim fileencoding= modeline.

M2 - the Go and TypeScript bodies are stale against their own heads. Go says eight tests and "8/8"; there are nine, and TestBuildDirectivesAreNotRespaced, the round-1 ask, appears nowhere in the body, the table or the mutation proof. TypeScript says 25 tests with every mutation row reading [24 skipped, 1 passed]; jest reports 26, the shebang rule is absent from the table, and npm test gives 1262 passed against the claimed 1261. The TypeScript mutation evidence was recorded before the shebang test existed.

M3 - the sixteen-file ruff gap pairs two different trees. Measured: 0.15.8 gives 124 on main and 125 on head; 0.16.6 gives 139 on main and 140 on head. The gap is 15 on either tree. The body's 124 against 140 crosses main and head, which is the same error its own paragraph warns about, one axis over.

M4 - Go has no shebang rule at all, with five shebang-bearing files, while both siblings have one. Unjustified drift.

Low

L1 Python's NOT_SOURCE detects virtual environments by name (.venv, venv). A venv named .rvenv put 5 of 12 tests into failure on third-party content, which is the fail-on-correct-code outcome the comment itself warns about. Detect pyvenv.cfg instead. L2 Shebang-in-comment false positive in both TypeScript and Python: one flagged inside a block comment, one inside a docstring. L3 No guard asserts the executable bit although all three cite "stops being directly executable"; TypeScript's scripts/postinstall.js is already mode 644. L4 Python's reader has 16 cases against 17 in the siblings, missing busl in markup. L5 An unenumerated third licence in prose (Eclipse Public License 2.0, no tag) passes everywhere, correctly disclosed as incomplete by construction rather than a defect.

Cross-repo coherence

Forced by the language, verified: Go asserts no packaging fact because go.mod has no licence field, and that is precisely why that repo could sit MIT-by-LICENSE and BUSL-by-header for a year. Python's two packaging facts against TypeScript's one is forced by packaging, and both Python mutants fail on distinct assertions, each independent of the header rule, which is the independence master asked for. Python's three positional rules are forced: Go has no cookie, TypeScript no PEP 263.

Drift: H1, M4 and L4. The rule pair, identifier plus prose, is identical and complete in all three, and all three assert third-party notices through the same predicate with a seen > 0 anti-vacuity floor, so that half is coherent.

Board

Go CLEAN, 14 contexts all SUCCESS with Integration Tests skipped. TypeScript and Python BLOCKED on Runtime E2E required for user-facing changes FAILURE, which is marker-only and not a code defect: only Go's title carries [skip-runtime-e2e]. It will not clear by editing the title, since definition-of-done.yml omits edited and a re-run replays the original payload; a new push is required. Python's test (3.11) had not reported. Hygiene clean in all three: one DCO-signed commit each, subjects 50, 45 and 53 characters after the prefix, no AI attribution.

What is required

Go: H2, M2, M4. TypeScript: H1, H2, M2, plus the skip marker applied by amend. Python: M1, M3, plus the marker. The Lows are cheap and better taken now than in a fourth repository. Round 2 on the pushes; master re-applies the shebang matrix and the PEP 263 plant.

@saurabhjain1592
saurabhjain1592 force-pushed the chore/license-headers-mit branch 2 times, most recently from 3454fdc to 0299355 Compare September 3, 2026 17:41
@saurabhjain1592 saurabhjain1592 changed the title chore(license): correct two Apache-2.0 headers and the LICENSE holder chore(license): correct two Apache-2.0 headers and the LICENSE holder [skip-runtime-e2e] Sep 3, 2026
LICENSE has read "MIT License" since the initial commit, but
axonflow/hitl.py and tests/test_hitl.py declared Apache-2.0. Neither was
ever a relicence question: both files were MIT all along and the headers
were wrong statements about them.

LICENSE's holder line also read "getaxonflow" where every sibling SDK
reads "AxonFlow". The name is now AxonFlow in all five; the year stays
each repository's own first year, so this one keeps 2025.

test_license_metadata.py pins the result. Two licence rules are needed
because each is blind to what the other catches: an identifier rule
cannot see an Apache prose block and a prose rule cannot see a bare tag.
Two more assert the packaging metadata PyPI publishes -- the `license`
field and the MIT trove classifier -- because LICENSE, the field and the
classifier can disagree and different consumers read different ones.

Three rules are positional rather than textual, because a licence sweep
rewrites the top of a file and that is where Python keeps meaning in
position: a shebang is only a shebang on line 1, a PEP 263 coding cookie
is honoured only on line 1 or on line 2 after a shebang, and a shebang
with a space after the hash is inert while also being invisible to the
position rule. A header pushed above a cookie makes the file decode as
UTF-8 silently, and nothing fails until a non-ASCII literal misbehaves.
The sibling Go SDK shipped the equivalent defect into review with build,
vet and its whole suite passing.

Ten mutants, each shown red.

No behaviour change: both modified files are byte-identical to main once
the leading comment run is removed. ruff format, ruff check and all 1378
tests pass.

Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
@saurabhjain1592
saurabhjain1592 force-pushed the chore/license-headers-mit branch from 0299355 to 9350797 Compare September 3, 2026 18:02
@saurabhjain1592
saurabhjain1592 merged commit df9511f into main Sep 3, 2026
15 checks passed
@saurabhjain1592
saurabhjain1592 deleted the chore/license-headers-mit branch September 3, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(license): two hitl files carry an Apache-2.0 SPDX header inside the MIT SDK; holder name differs

1 participant