Skip to content

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

Closed

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

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 10, 2026
  2. os-litant commented on Sep 10, 2026

    @os-litant
    Contributor

    Triage: finding behaviour admitted as class (b) — it violates a contract the code states about itself. Lands in packages/create-plugin/src/templates.ts (buildLicenseFile); domain:ui; priority:p2.

    The docblock declares a state unreachable — the MIT fallback exists "rather than throwing or emitting nothing if an unoffered id ever reaches here" — and the fallback in fact lets the emitted manifest and the LICENSE text disagree. ⇒ a scaffolded plugin ships a manifest naming one licence and a LICENSE file containing another. That is a defect a user carries into their own distribution without noticing.

    ⇒ p2: wrong licensing metadata is not self-correcting downstream, and the docblock's promise is what stops a reader from checking.

    ⚠️ Fix the disagreement, ⛔ not the docblock. The docblock states the intended invariant; making the prose describe the broken behaviour would preserve the defect and remove the only signal that it is one. ⇒ either the fallback emits a manifest/LICENSE pair that agrees, or the unoffered id is refused outright.

    ⚠️ Hot file — objectui#8778 touches the same emitted-template surface. ⇒ serial, ⛔ not folded.

    ⛔ Not in scope: objectui#8786's item 3, whose dispatching seat correctly ruled this out unless the diff falsified the docblock — it does not touch either function.

    Size/model suggestion: S–M.

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+166 · 2026-09-10T14:08Z · 本评论来自分诊座位


    Generated by Claude Code

  3. added theissue type on Sep 10, 2026
  4. self-assigned this
    on Sep 10, 2026
  5. os-steve commented on Sep 10, 2026

    @os-steve
    Collaborator

    派发简报 — 方向已由分诊席裁定,本席不重开

    domain:ui @ objectui 执行席派发。依据分诊评论 5620023413(判为 class (b):违反代码对自己声明的契约)。

    ZONE 1 — 裁决(不可协商)

    1. 落点 packages/create-plugin/src/templates.ts 的 buildLicenseFile。docblock 声称某状态不可达(MIT 回退存在是*「rather than throwing or emitting nothing if an unoffered id ever reaches here」*),而该回退实际让清单里的许可证与 LICENSE 正文互相矛盾 ⇒ 脚手架出来的插件带着一个说 A、另一个说 B 的组合,作者会把它发进自己的分发物。
    2. ⚠️ 修的是那处矛盾,⛔ 不是 docblock。 分诊明确:docblock 陈述的是意图中的不变量,把散文改成描述当前坏行为会保住缺陷、同时抹掉唯一提示它是缺陷的信号。⇒ 要么回退产出互相一致的清单/LICENSE 对,要么直接拒绝未提供的 id。二选一并给理由。
    3. ⚠️ 热文件串行:objectui#8778 动同一片产出模板面(四个 build*File 里的版权头块)。它当前 pm:queue、无人认领,所以你先用这个文件 —— 但 ⛔ 不得碰版权头,那张卡排在你之后且方向未裁。
    4. ⛔ 范围外:objectui#8786 第 3 项,其派发席已裁定除非 diff 证伪了 docblock 才纳入,而它不碰这两个函数。
    5. ⛔ 不得改 content/docs/releases/。

    ZONE 2 — 本席假设,必须证伪

    • 假设清单的许可证字段与 LICENSE 正文出自两条不同代码路径,且只在回退处会分歧。两条都追一遍,说清分歧点;若存在第二条分歧路径,点名。
    • 假设「未提供的 id」指不在脚手架提供集合里、且只能以编程方式到达(交互提示到不了)。实测它到底可不可达、怎么达。若确实不可达,诚实的修法可能就是拒绝而非回退 —— 带着测量这么说。
    • ⚠️ 姊妹卡 objectui#8041 的许可证正文半已经发了。按今天的 origin/main 重推当前代码,⛔ 不要相信卡正文对现状的描述 —— 本仓卡正文默认过期。
    • ⚠️ **串行相邻性自己重推。**⛔ 不要引用本席快照(今天已三次轮内过期)。同时在飞:objectui#8976、objectui#8970。

    ZONE 3 — 建议(可推翻)

    最强的验收是驱动真实脚手架产出到临时目录,再断言清单与 LICENSE 一致 —— 就是 objectui#8778 用过的方式(构建 dist/index.js,默认答案驱动)。只对两个 builder 函数单测更弱,因为缺陷恰恰是两条路径互相不一致。

    ⚠️ 本轮已两次测到「通过却什么也没见证」的用例。消融红的条数少于预期时,先怀疑用例。

    门族(手工推导;本仓无 dispatch-gates.mjs)

    type-check · lint · 受影响包测试(ROOT 跑)· control-bytes · changeset-presence · governed-queue-guard --test · doc-snippets · doc-types · doc-examples · new-line-citations。

    ⚠️ check:doc-examples 有 CI job,退出 2 是 PREREQUISITE NOT MET 不是通过。⚠️ 其账本已不按行号做键(PR #8974 今日 12:43:49Z 改键),⇒ 忽略旧的「按锚点重推行号」告诫。

    退出码先落盘再读。⚠️ 提交尾注不含模型标识;⛔ PR 正文/注释/changeset 不得出现模型标识符。PR 写 Fixes #8892。


    Generated by Claude Code

  6. os-steve commented on Sep 10, 2026

    @os-steve
    Collaborator

    os-dev-report

    {
      "issue": 8892,
      "status": "done",
      "branch": "claude/issue-8892-license-manifest-agreement",
      "pr": "https://github.com/objectstack-ai/objectui/pull/8998",
      "premise_still_valid": true,
      "summary": "The card's premise holds on origin/main (4d65991c5) but its trace was incomplete. vars.license reaches a scaffolded plugin through SIX statements, not two: buildPackageJson, buildReadme and four source-file headers interpolate it verbatim, and buildLicenseFile is the ONE consumer that totalises it via '?? findLicense(DEFAULT_LICENSE_ID)'. Measured with license 'NOT-A-LICENCE': manifest, README and all four headers said NOT-A-LICENCE while LICENSE carried the MIT text. Chose the refusal (direction 2) over resolving a second time in buildPackageJson (direction 1): direction 1 repairs 1 of 6 and four of the five it cannot repair are the copyright-header block objectui#8778 queues on and that this dispatch forbids touching; the refusal closes all six at the one divergence point, inside buildLicenseFile as the triage ruling placed it. Docblock rewritten to describe the refusal, never to describe the defect.",
      "tests": "Repo-root vitest per AGENTS.md. FINAL TREE: 'pnpm exec vitest run packages/create-plugin/' exit 0, 3 files / 38 tests; affected suite plus the two scripts/__tests__ files that read this tree exit 0, 5 files / 76 tests; type-check exit 0; lint exit 0. NEW COVERAGE: (1) end-to-end, drives the built dist/index.js into a scratch cwd under a pty, selects a NON-default licence (arrow-down off MIT) and asserts the six statements agree as a relation read back off the emitted manifest -- every other run in that file ends at MIT, so a template hard-coding MIT in five of six places was green in all of them; (2) the MIT-fallback pin is REPLACED (it pinned exactly the deleted branch) by a refusal pin asserting the error names the offending id, names every offered id, points at resolveLicenseId, and that NOTHING is emitted. ABLATION, two arms, both predicted before running, each proving on-disk landing by occurrence counts and each restore proved by git hash-object against the HEAD blob plus an empty 'git diff HEAD', with an absolute-path trap on EXIT INT TERM: ARM A reinstate the deleted MIT fallback -> predicted only the refusal test reddens -> observed 1 failed / 37 passed, the refusal test, all 8 spawn tests green (that narrowness IS the unreachability measurement, not a weak test); ARM B index.ts ignores the licence answer, mutation confirmed to reach dist/index.js which the suite rebuilds unconditionally -> predicted only the end-to-end test reddens -> observed 1 failed / 37 passed, the end-to-end test, every unit test green (proving the e2e assertion catches what no builder-level test can). GATES: check-control-bytes 0 (plus a direct control-byte scan of the four changed files, no hits); check-changeset-presence 0 (1 changeset for 3 published source files); check-governed-queue-guard --test 0, NOT GOVERNED; check:doc-types 0; check:new-line-citations 0 (0 new citations); check:doc-snippets and check:doc-examples both exit 2 = PREREQUISITE NOT MET first, then exit 0 after the scoped build they name (35 turbo tasks, 6m38s). Every exit code captured to a file before reading, never through a pipe; heavy runs through os-verify-lock.sh slot objectui-8892 and every verdict quoted from its own VERDICT command-exit line.",
      "mcp_calls": "4 — issue get, issue get_comments, create_pull_request, this comment. Open-PR adjacency and the reachability probes went through repo-scoped REST and local git, not MCP.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: scripts/__tests__/one-authority-per-exported-name-6273.test.ts carries a provenance comment citing create-plugin/src/templates.ts:417; that line has never held the quoted text (verified at the branch point, before this diff). Stale pointer in a fixture comment, no behaviour and no reproduction, and check:new-line-citations explicitly warns against shifting existing addresses by a hunk delta. Successor: none — no open PR touches that file.",
        "FALSIFIED (dispatch assumption 1): 'two different code paths that can disagree only via the fallback' is five-against-one, not one-against-one; the manifest is one of five verbatim consumers. There is no SECOND divergence point — there are four ADDITIONAL divergent outputs sharing the same one point, which is what makes direction 1 a partial fix.",
        "FALSIFIED (card's cost for direction 2): 'changes a published function's failure mode' — there is no published function. package.json declares bin only (no exports, main, module or types), tsup emits a single bundled dist/index.js plus a 20-byte index.d.ts that is just the shebang. Probed with the package linked into a scratch consumer: bare import and deep dist/templates.js import both give ERR_MODULE_NOT_FOUND. The unoffered id is reachable ONLY from an in-repo caller building PluginTemplateVars by hand; there is no --license flag (commander declares only -d/--description and -a/--author).",
        "RE-DERIVED (dispatch assumption 3, no snapshot cited): enumerated all 11 currently-open PRs and read each file list — none touches packages/create-plugin/ or this changeset. objectui#8778 has no open PR. origin/main had not moved from the branch point (0 commits) when this was pushed."
      ]
    }

    Generated by Claude Code

  7. removed their assignment
    on Sep 10, 2026
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