License policy¶
SynthOrg ships under the Business Source License 1.1, converting to
Apache License 2.0 after the Change Date (see LICENSE). To keep
redistribution unencumbered, the dependency set is held to a
copyleft-exclusion policy enforced by scripts/check_license_compat.py
(pre-push and CI).
Rules¶
- AGPL and GPL (non-LGPL) are forbidden. No strong-copyleft package
may enter a shipped dependency table.
pymupdf/fitz/pymupdf4llm(AGPL-3.0) are named on a hard denylist and must not appear inpyproject.tomlor in the resolveduv.lockclosure at any depth. - LGPL is permitted with attribution. Weak-copyleft (LGPL-3.0)
components are dynamically linked / imported as separate works, so they
may ship, but every LGPL distribution MUST be attributed in the
top-level
NOTICEfile. This applies to both Python and JS deps: today the Python side ispsycopg,psycopg_pool, andpsycopg_binary(the optionalpostgresextra and the published backend image), and the JS side covers any LGPL package discovered dynamically inweb/package-lock.json. The Python_KNOWN_LGPLset is a curated backstop list; JS LGPL deps are not curated but are detected from the lockfile's per-package SPDXlicensefield. - GPL developer tooling stays external.
golangci-lint(GPL-3.0) is installed as a standalone binary and is never added as ago tooldirective, so its transitive closure never enterscli/go.mod/cli/go.sum. Python developer linters with GPL licences (codespell,yamllint) live only in thedependency-groupsdev set and are never shipped to a consumer.
What the gate checks¶
- Denylist --
pyproject.tomldependency tables and the fulluv.lockpackage set are parsed (viatomllib, so a prose comment that merely names a package does not trip the gate) and matched against the hard denylist. - Go GPL exclusion --
cli/go.modandcli/go.sumare scanned forgolangci-lint. - Direct-dependency copyleft scan -- every direct runtime / extras
dependency declared in
pyproject.tomlis classified from its structured licence metadata (the SPDXLicense-Expressionand theLicense ::trove classifiers, never the freeform licence text). AGPL / GPL is a hard failure; LGPL requiresNOTICEcoverage. A curated known-LGPL set is also asserted againstNOTICEso the attribution check holds even when thepostgresextra is not synced into the gate's environment. - Web JS copyleft scan -- every package in
web/package-lock.jsonis classified from its per-entry SPDXlicensefield. AGPL / GPL (non-LGPL) is a hard failure; an LGPL package must be attributed inNOTICE(matched by npm package name, scoped or plain) or the gate fails. A missingweb/package-lock.jsonis tolerated (the scan yields no violations) so the gate stays usable before the lockfile exists. - Elected disjunctions -- a dist named in
_ELECTED_DISJUNCTIVEis resolved from its installed metadata and checked two ways: the offer must still reach the arm this project elected, and that election must be recorded inNOTICE. An unresolvable dist is a violation rather than a skip, because absence means the environment cannot answer the question the check exists to ask.
Transitive copyleft of unknown packages is covered by the name denylist
over the full uv.lock closure rather than by classifying every
transitive distribution: transitive licence metadata is too unreliable
to classify by scanning (a permissive package's bundled-component
attribution text routinely names other licences). When a genuinely new
copyleft dependency needs handling, add it to the denylist (to exclude)
or to NOTICE plus the known-LGPL set (to attribute) in the same change.
Disjunctive licences¶
An SPDX expression may be a DISJUNCTION -- MPL-1.1 OR GPL-2.0-only OR
LGPL-2.1-or-later -- which offers alternatives rather than stacking
obligations. The licensee elects one arm and is bound by that arm alone,
so matching against the whole expression answers a different question
than the one being asked: it would reject a package that is compatible
under an arm we can take, or accept one whose only usable-looking arm we
cannot.
The gate therefore classifies each arm and takes the least restrictive,
which is the arm a licensee elects. Splitting is on the SPACE-DELIMITED
operator, never a word boundary: GPL-3.0-or-later is one licence whose
name happens to contain or, and splitting inside it yields an arm that
classifies as permissive, quietly passing the strongest copyleft there
is.
Only the SPDX expression is split. The two structured sources answer
the question differently and must not be concatenated before splitting.
License-Expression is SPDX, so OR in it is the operator; a trove
classifier is prose that happens to contain the word, and the canonical
LGPL one reads GNU Library or Lesser General Public License (LGPL).
Split on the operator it yields the arm gnu library, which matches no
family, reads permissive, and wins the least-restrictive rule, taking an
LGPL dependency straight past the NOTICE requirement that exists for
precisely that licence. Classifiers are therefore classified whole and
the most restrictive governs, and the expression wins when a dist
declares both, since PEP 639 makes it the authoritative field.
Two consequences worth knowing:
- The direct scan reads
pyproject.toml, so a dist reached transitively is classified by nothing unless it is named.tldarrives throughtrafilatura->courlanand is listed in_ELECTED_DISJUNCTIVEfor exactly that reason; without the entry the disjunction handling would never run against the package it was written for, andNOTICE's election would be prose with no check behind it. - The elected arm is re-verified on every run, not just recorded once. A version bump can drop an arm while the package name stays put, which leaves a dependency nobody may redistribute sitting behind a green gate; the name denylist cannot see that, because nothing about the name changed.
- That re-verification asks for MEMBERSHIP, not restrictiveness. The
declared election is the single owner of which arm this project takes,
and
_classifyindependently resolves a disjunction to its weakest arm, which fortldis MPL-1.1 whileNOTICEelects LGPL-2.1-or-later. Comparing families would also pass an offer that had dropped to MPL-1.1 alone, since that is less restrictive than the elected arm, while the armNOTICEnames had ceased to exist.
Re-linking LGPL components¶
The LGPL-3.0 obligation to allow substitution of the linked library is
satisfied as described in NOTICE: rebuild the backend image with a
replacement psycopg wheel in place of the pinned version. The
psycopg_binary C extension is an optional acceleration of the
pure-Python psycopg, which can be used standalone.