Skip to content

bug(create-plugin): buildLicenseFile's MIT fallback lets the emitted manifest and LICENSE text disagree, against its own docblock #8892

Description

@claude

Found while implementing objectui#8786 (director-seat follow-up card, item 3 of its body). ⛔ Deliberately NOT carried in that card's PR — the dispatching seat ruled it in scope only if that diff falsified the docblock, and it does not touch either function.

The declared contract

packages/create-plugin/src/templates.ts, the docblock over buildLicenseFile, says the MIT fallback exists so that one state is unreachable — quoted verbatim:

Falls back to DEFAULT_LICENSE_ID's text rather than throwing or emitting nothing if an unoffered id ever reaches here. resolveLicenseId already makes that unreachable from the CLI; the point of the fallback is that the ONE state this card exists to remove — a manifest claiming a licence with no text beside it — must not be reachable by any route, including a future caller that builds PluginTemplateVars by hand. It is not a lenient alias for bad input: the manifest is written from the same resolved id, so the two still agree.

Why the last sentence is false

buildLicenseFile totalises its input (findLicense(vars.license) ?? findLicense(DEFAULT_LICENSE_ID)), but buildPackageJson writes vars.license verbatim into the emitted manifest. Nothing resolves it a second time. So the two do NOT come from the same resolved id, and the named caller — one that builds PluginTemplateVars by hand — reaches exactly the state the docblock says is unreachable:

const files = buildPluginFiles({ /* … */ license: 'NOT-A-LICENCE' });
// files['package.json'] declares "license": "NOT-A-LICENCE"
// files['LICENSE']      carries the MIT text

A manifest claiming a licence beside text for a different one is the same defect class objectui#8041 exists to remove, arrived at by the route the docblock names.

Boundary — this is not user-visible today

@object-ui/create-plugin is bin-only (no exports, no main), and the CLI is the only caller: it passes resolveLicenseId(answers.license), which is total onto the four offered ids. So there is no route from a real scaffold to a disagreeing pair. What is wrong is the contract: a docblock that states an invariant the code does not hold, in the one file whose whole subject is that the manifest and the text are one decision read twice.

Directions (⛔ not ruled — the maintainer's or the director seat's call)

  1. Make buildPackageJson write the same resolved id (resolveLicenseId(vars.license)), so the docblock becomes true and the fallback is genuinely total.
  2. Make buildLicenseFile throw on an unoffered id instead of falling back, so the disagreement is impossible rather than silent — the docblock then needs rewriting, since it argues for the fallback.
  3. Correct the docblock to what the code does: the fallback keeps a LICENSE from being missing, not from disagreeing.

Direction 1 is the smallest and keeps both the fallback and the sentence. Direction 2 is the contract-first shape (reject bad input at the boundary rather than coercing it) but changes a published function's failure mode.

⛔ Out of scope here: the licence set itself (four licences, no free text — the maintainer's call), and objectui#8778 (copyright holder in the four emitted source headers), which stays separate.


Filed by the domain:ui dev seat while working objectui#8786; not labelled, for PM triage.


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions