Skip to content

build: make LICENSE the authority for non-npm third-party notices - #3325

Draft
Astro-Han wants to merge 3 commits into
mainfrom
docs/3270-third-party-attribution
Draft

build: make LICENSE the authority for non-npm third-party notices#3325
Astro-Han wants to merge 3 commits into
mainfrom
docs/3270-third-party-attribution

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Per-artifact third-party notices are generated from the npm production dependency closure, which by construction cannot reach vendored, adapted, or generated-from-upstream material. The root LICENSE carried that material as prose, nothing checked it against the tree, and one defect copied all of it into a single package's notice.

This makes LICENSE the authority. The generator parses its THIRD-PARTY COMPONENTS section, reproduces every entry verbatim into both notices, and fails if an entry and its carrying file disagree — in either direction. All four entries now reach the notices instead of the two a hand-written list would name, any license works (Geist is OFL-1.1), and trycua/cua gets anchored for the first time.

  1. fix(scripts)@ai-sdk/provider-utils ships no license file, so the generator substituted the whole root LICENSE as its Apache-2.0 text, filing 140 lines about unrelated third parties under one package. Truncate at the section header, fail closed if it is missing. Prerequisite here, since commit 2's entries would otherwise ship inside that notice. Regression test comes from docs: close code origin audit #2907, which carries the same fix.
  2. docs — attribution headers in tool-output.ts and edit-replace.ts, scoped to the adapted material (both files are mixed-origin, so no whole-file SPDX), plus the two LICENSE entries.
  3. build(scripts) — the LICENSE parser, the carrier map, and CI wiring.

Refs #3270 — closes its first exit criterion. Refs #2907.

Review focus

  • The models.dev entry states what is copied and draws no legal conclusion. An earlier draft called the records uncopyrightable facts carried over as "selection and arrangement". Both halves were wrong: the generator copies model.name and model.description verbatim (1779 upstream-authored descriptions), and the selection and ordering are the generator's own. MIT was always reproduced in full, so the obligation never rested on that sentence — only its accuracy did.
  • The opencode entry records the whole upstream chain and deliberately does not adjudicate it. opencode's edit.ts credits gemini-cli's editCorrector, which is Copyright 2025 Google LLC under Apache-2.0; its escape set shares the first eight branches, in order, with the replacer here. Whether that carries protected expression is a licensing call, not mine.
  • Both-ways validation constrains what lands next. docs: close code origin audit #2907's Vercel AI SDK and Astryx entries cannot land unanchored — a LICENSE entry with no carrier decision fails generation.
  • Whichever of docs: close code origin audit #2907 / this PR lands second must regenerate both notices and drop the duplicated two-line fix.

Known limits

Proves LICENSE's declarations are still true in the tree; does not discover undeclared material. That scan's allowlist is #2907's audit table, so it belongs there.

Verification

Both notice targets regenerated and both check scripts verified — the desktop one passes locally while the CLI one drifts.

Every failure path falsified end to end against the real tree: removed a carrier's marker, added a LICENSE entry with no carrier, removed a LICENSE entry with its carrier left behind, renamed the section header. All four fail with the specific error; each reverted immediately after.

Upstream copyright lines checked against the upstream LICENSE files rather than transcribed. Generator determinism checked: two runs against the pinned api.json are byte-identical, and the digest now hashes the extracted payload rather than api.json, so dropped providers no longer produce header-only diffs. The generated files differ from main by +6 lines, −0 — header only, bodies byte-identical.

format:check, lint, core/runtime typecheck, 12 script tests, 26 runtime tests — all clean. Not run: the full repository suite.

AI use

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Claude Code drafted the attribution text, LICENSE entries, parser, carrier map, and tests, ran every check above, and coordinated two adversarial reviews (Codex and a Claude subagent) whose findings drove the LICENSE-as-authority redesign. The upstream comparisons against opencode fc80874 and gemini-cli, the license determinations, and the decision to leave copyrightability unadjudicated were reviewed by the contributor of record.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

No runtime behavior changes; the two runtime files gain comments only. Both THIRD_PARTY_NOTICES.txt artifacts do change content, which is the point, and check:release, the CI planner's CLI-package inputs, and the Windows release path filter gain entries.

@Astro-Han
Astro-Han force-pushed the chore/models-dev-refresh-catalog-snapshot branch 3 times, most recently from 31a9326 to 8aa2935 Compare August 20, 2026 14:44
@Astro-Han
Astro-Han force-pushed the docs/3270-third-party-attribution branch 2 times, most recently from 3eab973 to e4bdb38 Compare August 20, 2026 17:43
@Astro-Han
Astro-Han force-pushed the chore/models-dev-refresh-catalog-snapshot branch from 7e64456 to 8dca808 Compare August 20, 2026 17:48
@Astro-Han
Astro-Han force-pushed the docs/3270-third-party-attribution branch from e4bdb38 to 78dbe2c Compare August 20, 2026 17:49
Base automatically changed from chore/models-dev-refresh-catalog-snapshot to main August 20, 2026 18:34
@Astro-Han
Astro-Han force-pushed the docs/3270-third-party-attribution branch from 78dbe2c to c34f687 Compare August 20, 2026 18:35
…tice

`@ai-sdk/provider-utils` ships no license file, so the generator substitutes the
root LICENSE as its Apache-2.0 text. It substituted the whole file. LICENSE also
carries Maka's THIRD-PARTY COMPONENTS section, so 140 lines describing
trycua/cua and the bundled fonts were filed under one npm package in both
release notices — attributing material to a party that has nothing to do with
it, in the artifact whose only job is to attribute correctly.

Truncate at the section header. `split` returns the whole string when the
delimiter is absent, which would silently reinstate this exact bug and leave
`--check` demanding that the wrong output be committed, so the delimiter is
required rather than assumed.

The regression test comes from PR #2907, which found this defect and carries the
same fix. It deliberately matches the package without its version, so a routine
dependency bump does not fail it. Both are repeated here because this branch
adds LICENSE entries, and without the fix those entries would ship inside the
provider-utils notice. Whichever lands second drops the duplicate.

The test is wired into `check:release` and into the CI planner's CLI-package
inputs, so it runs on pull requests that touch it rather than only at release
preparation.

Generated-by: Claude Code
The code origin audit (#2907) found two pieces of third-party material that ship
in every release and appear in none of Maka's attribution surfaces, both for the
same structural reason: neither is an npm package, and the notice generator
walks npm production dependency trees only.

`tool-output.ts` and `edit-replace.ts` already said in prose that they adapt
opencode, and the adaptation is real — the escape-normalizing replacer's regular
expression and its nine branches are carried over unchanged. Prose is not the
notice MIT requires. Each file now records source, revision, license, and
copyright, and LICENSE carries the permission notice so it travels with the
release. Both files are mixed-origin, so the notice is scoped to the adapted
material and neither gets a whole-file SPDX identifier.

The LICENSE entry records the whole upstream chain rather than stopping at the
nearest one. opencode's edit.ts credits cline diff-apply and the gemini-cli
editCorrector; that file is Copyright 2025 Google LLC under Apache-2.0, and its
escape set shares its first eight branches, in order, with the replacer here.
opencode added the ninth and changed the pattern. Whether that shared sequence
carries protected expression is deliberately not adjudicated in LICENSE — it is
recorded so a licensing review can see the chain and decide.

The models.dev entry states what is copied instead of characterising it. The
earlier draft said the individual records were uncopyrightable facts and that
what came from upstream was the selection and arrangement. Both halves were
wrong: `sync-model-metadata.mjs` copies `model.name` and `model.description`
verbatim, and the snapshot carries 1779 upstream-authored descriptions; while
the selection and the ordering are the generator's, not upstream's. The entry
now lists the fields actually carried and leaves the legal conclusion out. The
MIT text was already reproduced in full, so nothing about the obligation
changes — only the accuracy of the sentence next to it.

Upstream copyright lines were checked against the upstream LICENSE files rather
than transcribed: both are `Copyright (c) 2025 opencode` and `Copyright (c) 2025
models.dev` verbatim.

The generated headers name their payload. models.dev serves a rolling document
with no version, so the generator stamps the sha256 of the data it extracted —
not of api.json, which would move whenever any of the ~40 dropped providers
changed and produce header-only diffs that say nothing about the file they sit
in. Hashing after extraction makes the digest describe what ships and keeps the
generator a pure function of its input: the same api.json produces the same
bytes on any day. Verified by running it twice against the pinned payload.

Closes the first exit criterion of #3270.

Generated-by: Claude Code
…ifact notice

The generator's inventory is the npm production dependency closure. That is a
complete answer to "which npm packages ship" and a structurally blind one to
"what else ships": vendored, adapted, and generated-from-upstream material is
not a package, so no amount of checking the walk's output can reach it. The
previous commit's LICENSE entries were guarded by nothing — an attribution
header could be dropped in a routine edit and both notices would still
regenerate clean.

The obvious move is to declare that material in the generator. The first draft
did, and it was wrong: it restated the repository, revision, copyright, and
license text that LICENSE already holds, creating a second authority that could
disagree with the first while every check passed. Passing a deliberately wrong
repository and revision to that validator was accepted without complaint, and
the notice printed them.

LICENSE is the authority. ASF release policy puts the notice there, it ships
with every artifact, and a human reviews it — and its THIRD-PARTY COMPONENTS
section is already structured. So the generator parses it and reproduces the
entries verbatim into both notices, and this module keeps only the one fact
LICENSE cannot express: which file carries each entry and what text must still
be in it.

That inverts what the check is worth:

- All four entries reach the notices, not the two a hand-written list happened
  to name. Geist is OFL-1.1 and was structurally undeclarable before, because
  the draft could only emit MIT.
- License text is LICENSE's own, not rebuilt from a template around a
  hand-transcribed copyright line.
- The check runs both ways. A LICENSE entry with no carrier decision fails, so
  #2907's Vercel AI SDK and Astryx entries cannot land unanchored; a carrier
  decision with no LICENSE entry fails too.
- `trycua/cua` is now anchored. `palette.ts` calls itself a "faithful 1:1 port"
  — a stronger claim than the opencode adaptation — and nothing had been
  holding it.

The notices' own prose no longer implies more than it delivers: it says the
entries are LICENSE's section reproduced and checked against their carriers,
not that everything vendored has been found. Discovering undeclared material is
a scan whose allowlist is #2907's audit table, and it belongs there.

Validation takes its inputs as arguments, so every failure path is exercised
against fixtures rather than by editing tracked source. `non-npm-sources.mjs`
and both test files are wired into the CI planner's CLI-package inputs, the
Windows release path filter, and `check:release`, so changing the guard runs
the guard.

Generated-by: Claude Code
@Astro-Han
Astro-Han force-pushed the docs/3270-third-party-attribution branch from c34f687 to 8311320 Compare August 20, 2026 20:32
@Astro-Han Astro-Han changed the title docs: attribute the adapted opencode sources and the models.dev snapshot build: make LICENSE the authority for non-npm third-party notices Aug 21, 2026
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.

1 participant