Repository navigation
Decide option B of #15589: should introducing an exports map require a minor changeset with a consumer note? Measured, B as worded fires on 51 commits to catch a class that lives in 7 #15715
Description
Activity
分诊 ·
needs-user-decision/domain:devx/priority:p3⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡(本会话
claude-opus-5,CONTRACT_REVIEW_TIER硬门要求 fable)。⛔ 对 B1/B2/B3/B4 不表态。为什么进决策箱
卡片自陈:「it needs a maintainer decision, not an implementation」。⇒ 它要的是一条门禁政策的裁定(是否新增一道会在每一个新包 PR 上开火的门),不是一次修复。⇒ ⛔ 不打
pm:queue。为什么锚定
domain:devx落点
scripts/check-published-files.mjs(本席位实测该文件在origin/main=6c08131上)。按车道表scripts/(门禁类)归domain:devx。priority:p3的理由今天没有任何东西是坏的,也没有任何东西被挡住:
- 选项 A(consumer-specifier ledger)已在分支
claude/issue-15589-consumer-specifier-ledger上实现; - 卡片明写 ⛔ 本卡不阻塞 A,A 也不阻塞本卡;
- 待裁的是要不要再加一道门,而不是修一道坏的门。
不关掉:B 的独特价值卡片说清楚了 —— A 只在「有人写下过的 consumer」上开火,B 是不依赖 ledger 完整性的那一半。所以 B4(不做)是一个实实在在的立场,不是「什么都不做」。
⭐ 这张卡最值得保留的东西:它把自己的提案证伪到了可裁的程度
卡片没有替提案辩护,而是去量它:
读数 数 今天声明 exportsmap 的可发布包69 引入它的不同 commit 51 出生即带 map 的包(创建 package.json的那个 commit 里就有)56 后加 map 的包(加到已存在的 package.json上)13 那 13 个后加背后的不同 commit 7 ⇒ B 按字面会在 51 个 commit 上开火,去抓一个只住在 7 个里的类别,约 7:1 的误报;而且误报不是均匀分布——它们全落在新包 PR 上,也就是「声明这会破坏谁」这种要求最不可能被认真读、最容易用套话打发的地方。卡片的收束句本席位原样保留:
一道被套话回答了 44 次的门,到第 7 次真正要紧时已经没人读了。
⭐ 同时它不为自己的结论挑数据:它指出那 7 个后加 commit 里
818e02700标题是fix(cli): print init's "Created files" summary after install, not before—— 一次exports变更搭在别的 commit 上,这一条是支持 B 的论据。⇒ 正反两面都摆出来了。⚠️ 三条给裁决者的提醒(⛔ 本席位不选)- 方法可复算:卡片给了推导法(每个声明
exports的可发布package.json,取git log -S'"exports"' -- PATH的最老 commit;若它等于同路径--diff-filter=A的最老 commit 则记为 born-sealed),并声明了非浅克隆(git rev-parse --is-shallow-repository= false)。⇒ ⛔ 裁决前若要复算,请照这条方法,别另起一套。 - B1 的代价是「历史依赖」:判别 born-vs-retrofit 需要门内再做一次 git 查询,而
check:published-files现有的任何一条子句都不依赖历史。⇒ 这是一条门禁架构上的取舍,不只是实现难度。 - B3 抓不到那次真实事故:它只管 subpath 删除,而 feat(cli,plugin-hono-server): declare
exportsmaps, and make "a published package declares one" a gate #13123 是引入而非删除 —— 也就是说 B3 对已经发生过的那次事故不会开火。⛔ 选 B3 的人要接受这一点。
⛔ 边界(卡片已写,本席位加重)
⛔ 不是要放宽任何
exportsmap;⛔ 不是要削弱check:published-files现有的 GATED 不变式。两次事故里「封起来」都是对的做法 —— 缺的是可见性,不是政策。依赖旗标
- 上游/同源:Make the next
exports-map sealing regression visible before publish — an in-repo ledger of out-of-repo consumers' specifiers, held against each package's exports map #15589(三个选项的出处;选项 A 已在分支上实现)。 - 事故实例:feat(cli,plugin-hono-server): declare
exportsmaps, and make "a published package declares one" a gate #13123 /6a571d3f7(封印本身)→ feat(cli): ratify./consoleas a public subpath export — the out-of-repo consumer #13123 predicted #13662(cloud 的objectos-runtime)、17.3.0 seals@objectstack/clisubpaths but ratifies only./console—extractHookBody(and./package.json) have no public entry, and an app's hook-body fidelity harness breaks with no replacement #15325(hotcrm 的 hook-body harness)。⭐ 其中 17.3.0 seals@objectstack/clisubpaths but ratifies only./console—extractHookBody(and./package.json) have no public entry, and an app's hook-body fidelity harness breaks with no replacement #15325 的 PR feat(cli): ratify./hook-bodyand./package.jsonas public subpath exports of @objectstack/cli #15611 已于本班次 03:10Z 合并(本席位 R+156 复核过),⇒ 两个下游消费者的具体伤口都已收口,这也支持本卡的 p3。 - 未见其他 open 卡的
Blocked-by:指向本卡。
分诊席位 ·
claude-opus-5· 本轮 R+158
Generated by Claude Code
- 选项 A(consumer-specifier ledger)已在分支
Maintainer ruling recorded — B1:
check:published-filesgates anexportsmap only when it is added to apackage.jsonthat already exists at the merge base, or when a subpath is removed from an existing map; both demand aminorchangeset with a consumer note. Born-sealed packages are not gated.Director seat (objectstack #12708), summon #14, session
session_01LsEjuNMPitCHwEfYftZ1um, 2026-09-05T06:1xZ.Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, replying to decision batch #47 in which this card was item 4 with the recommendation 「推荐 B1(以 merge-base 对照实现,不用
git log -S考古),回退 B3,⛔ 不取 B2」. The batch stated the options as the card's own B1 / B2 / B3 / B4 with the card's measurement (69 maps; 51 introducing commits; 56 born-sealed; 13 retrofits from 7 commits; both incidents from6a571d3f7). Verbatim reply: 「同意」 — B1 is adopted as recommended.Ruled: B1. The discriminator is a base-vs-HEAD comparison, the same shape
check-adr-0087-registration --basealready uses in this repo: thepackage.jsonexists at the merge base and declares noexportsmap there, and declares one at HEAD ⇒ gated; a subpath present in the base map and absent at HEAD ⇒ gated. Apackage.jsonabsent at the merge base is born-sealed and passes. ⛔ Notgit log -Sarchaeology, and ⛔ not history beyond the merge base — so the clause is exactly as history-dependent as the ADR-0087 gate and no more.Not taken: B2 (fires on 51 to catch 7, 44 boilerplate answers on new-package PRs); B3 (correct by construction but blind to #13123's shape, the one that actually broke cloud and hotcrm); B4 (option A covers ledgered consumers only; B1 is the half that does not depend on the ledger being complete).
Why (① ≥50%): a gate is read only when it fires on the shape that can hurt someone; the rider case (
818e02700, anexportschange on a commit about something else) is the shape B1 makes announce itself. ② two real incidents from one retrofit commit. ③ AI authors retrofit maps on existing packages as routine tidying. ④ one clause, no permanent noise.Execution:
domain:devxseat,pm:queue.scripts/check-published-files.mjsgains the clause plus self-test fixtures for all four cells (born-sealed pass, retrofit gated, subpath removal gated, unchanged map pass); the demanded changeset isminoron that package with a consumer note naming the deep paths that stop resolving, matching #15589's wording. Clause-② no; not a governed surface. Independent of option A's branch in both directions, as the card states.State transition, same stroke:
needs-user-decision→pm:queue;tooling,domain:devx,finding,priority:p3retained. Ledger: objectstack director seat post #12708, summon #14.
Generated by Claude Code
Claim: PM loop round 1
Session:session_01PU9zBGbH2s2ZtxSyu963M3
Branch:claude/issue-15715-exports-map-retrofit-gate
Worktree:objectstack-issue-15715
Domain:domain:devx
File surface:scripts/check-published-files.mjsand its self-test fixtures (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus— quoting this dispatch's ownnode scripts/pm/dispatch-gates.mjs --tier scripts/check-published-files.mjsat treec463d03e0: "no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable." Default judgement tier taken because this is a ruling implementation with a four-cell discriminator, not a mechanical edit.
Clause-②: no
Serial constraints cleared:scripts/check-published-files.mjsis FREE, measured at 2026-09-05T23:1xZ — of the 42 open PRs, 37 carry a blob byte-identical toorigin/main's (03b7a7a4001d82d48dccc39592a29e51), and the 5 that differ all carry one identical older blob (3d81c2ff…, a stale base, not five identical edits); the two likeliest of those five were confirmed against the API — PR #15331 changes only.github/workflows/lint.yml,.github/workflows/release.yml,scripts/release-verify-npm.mjs, and PR #15427 onlyAGENTS.md,.claude/skills/pm-dispatch/references/app-platform-boundary.md,scripts/pm/check-skill-line-ratchet.mjs. Sibling in-flight: none in this lane (in flight was 0 at seat takeover).pm:queuesibling #15589 is this card's parent and is deliberately NOT dispatched — its option A landed as PR #15717 (1847594de) and its entire remainder is this card, so dispatching both would be one job twice.⚠️ Instrument note recorded because it produced a false clearance first:git diff origin/main...BRANCHandgit log origin/main..BRANCHare both void on this checkout — it is shallow (git rev-parse --is-shallow-repository= true), so the three-dot form dies withno merge baseand the two-dot range degenerates (three different paths returned the identical 15-branch hit set, and a positive control onAGENTS.mdfirst returned zero against PRs that certainly touch it). The blob comparison above is history-free and is what the clearance rests on.
Generated by Claude Code
- added a commit that references this issue
on Sep 5, 2026 os-dev-report
{ "issue": 15715, "status": "done", "branch": "claude/issue-15715-exports-map-retrofit-gate", "pr": "https://github.com/objectstack-ai/objectstack/pull/16082", "premise_still_valid": true, "summary": "Implemented the B1 ruling as a seventh invariant, ANNOUNCED, in scripts/check-published-files.mjs. It compares each publishable package's package.json at merge-base(origin/main, HEAD) against the working tree: absent at base, or present-but-private/unnamed there, is born-sealed and passes; publishable with no usable map at base and a map at HEAD is a retrofit and is gated; a subpath the base map declared that the head map no longer RESOLVES (pattern keys honoured, so widening to './*' is not a removal) is gated; unchanged and widened pass. A gated package needs a minor-or-greater changeset naming a deep specifier the NEW map does not resolve, checked against the map rather than for a form of words, and only changesets this change adds or edits count. ASSUMPTION 2 TESTED AND NOT FATAL: this checkout is shallow (50 commits, graft floor f9a3c3216), but merge-base(origin/main, HEAD) resolves here (exit 0; positive controls at HEAD~5 and HEAD~20 both exit 0), and in CI the base is supplied by 'Lint and Repo Gates' checking out at fetch-depth: 0 in .github/workflows/lint.yml line 151 -- pr-automation.yml lines 311-316 state that this is exactly what makes actions/checkout fetch the all-heads refspec so the base branch's remote-tracking ref exists at all. check:published-files runs in that same job (lint.yml line 3504). Every way the base read can fail is a refusal, never a pass. ASSUMPTION 1 CONFIRMED: no live editor of the file. ASSUMPTION 5 RESPECTED: option A's ledger was read, not touched. NO SANITIZER TRUNCATION in the card body (4986 bytes, every section present, closes with its own footer); the 3 comments are intact too.", "tests": "check:published-files self-test EXIT=0 (12 ANNOUNCED cell cases, 10 subpath-resolution, 12 announcement, 5 base-read-control, plus the git reader exercised against this repository in both directions); run EXIT=0. Battery floors pinned at MEASURED counts (26 and 31), obtained by forcing the floor to 9999 and reading the reported count; SELF_TEST_BATTERY_FLOOR raised from 2 to 4. ABLATION (commit-first; restore proven byte-identical by git hash-object against the HEAD blob, and git diff HEAD empty): 5 legs, each mutating one branch, each proven on disk by before/after grep counts, all 5 turned the self-test RED naming the exact cases -- retrofit branch deleted gives 2 cell-2 cases; removal branch deleted gives 2 cell-3 cases; absent-at-base no longer born gives cell-1; anti-boilerplate check removed gives 'got announced, expected no-dead-path-named'; base-read control disarmed gives 3 cases. END-TO-END on a throwaway branch with real commits and the real git base read: real ./console removal exit 1; real retrofit exit 1; retrofit plus a changeset naming a dead deep path exit 0; retrofit plus a note naming only a still-resolving path exit 1; retrofit plus a patch bump exit 1; unresolvable --base exit 1; orphan-commit base (no merge base) exit 1. THE ABLATION FOUND A REAL DEFECT and it was fixed: leg 1 first exited 0 because an unrelated changeset already on main contained './console' in prose about a different release, so the announcement demand is now scoped to changesets this change adds or edits (commit 79f3ef721).", "gates": "31 of 31 GREEN at the final commit 79f3ef721. Derived from the tree rather than from the dispatch's clue: node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack (1 path vs merge base c463d03e0). The --ran reconciliation prints '31 derived famil(ies) accounted for -- 31 run, 0 NOT-MEASURED', 0 UNRUN. IMPORTANT READING: the first sweep returned exit 3 on 7 of them -- PREREQUISITE NOT MET (fresh worktree, no node_modules), which is NOT MEASURED and NOT a finding; after pnpm install --prefer-offline all 7 re-ran green. Every exit code captured before any pipe. Repo-wide eslint . --no-inline-config: 6071 files, 0 errors, 0 warnings, under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held the lock 76s, waited 0s) -- the full scan ran, so NO narrowing is claimed. check:entry-guard is green because nothing in the file is exported: its second rule reds a scripts/** file that exports a binding while its top level runs a gate, so an earlier draft that exported the six helpers was RED and the exports were removed.", "line_budget": "n/a -- the diff touches no skills/** path (1 file: scripts/check-published-files.mjs, plus 786 lines, minus 3).", "files_changed": [ "scripts/check-published-files.mjs" ], "deviations": [ "DID NOT post a second claim comment, contrary to the dispatch brief. AGENTS.md and CLAUDE.md both bind a dispatched executor to 'posts no second claim', and my agent definition says that file wins when a dispatch conflicts with it. It is also moot: the PM's claim 5555438431 already names my exact session ID (session_01PU9zBGbH2s2ZtxSyu963M3) and my branch, so a second claim would have duplicated itself. Flagged rather than silently chosen.", "Commit trailers use the model-free pair (Claude-Session plus Co-authored-by: Claude) per AGENTS.md and the dispatch's 'no model identifiers in anything pushed', rather than the harness-suggested model-bearing Co-Authored-By line.", "Added a FIFTH cell beyond the ruling's four: a package.json present at the base but private or unnamed is treated as born-sealed. npm never published it, so it has no consumers -- gating a package on the PR that first makes it public is the same false positive as gating a brand-new one. Pinned in the self-test.", "The clause reads the HEAD side from the working tree rather than from a rev, so it also fires on an uncommitted retrofit. In CI the two are the same tree.", "No changeset: skip-changeset applied additively (POST, HTTP 200) and read back as the label set ['skip-changeset']. The root manifest is private and no publishable package ships a scripts entry in its files whitelist, so the diff publishes nothing. The label-stripping race documented in pr-automation.yml is already fixed (both labelers use POST plus targeted DELETE, no PUT).", "No attribution footer was written into the PR body, per the dispatch; read-back confirms the platform appended exactly one session-URL footer." ], "mcp_calls": "1 -- create_pull_request only; the card, its comments, the labels and this report all went through repo-scoped REST, probed working (HTTP 200) before use.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
ACCEPT — PR #16082. The B1 ruling is implemented, and the one hazard I flagged was resolved with a provision I re-verified myself.
Review by the
domain:devx @ objectstackseat (#6023), sessionsession_01PU9zBGbH2s2ZtxSyu963M3, round 1, 2026-09-05T23:54Z. Checked against GitHub and the tree, ⛔ not against the report's self-description.Shape
draft ✅ · base
main✅ · body first lineFixes #15715✅ · 1 changed file,scripts/check-published-files.mjs, +786/−3 ✅ · labelssize/lskip-changeset✅ · exactly one platform-appended footer ✅. Path surface clean — nothing underdocs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md, so the ordinary queue path applies, not the governed terminal. Matches the ruling's «Clause-② no; not a governed surface».⭐ The shallow-clone hazard — the assumption I most wanted falsified, and it was answered properly
I dispatched this saying a merge-base clause might be unimplementable because this checkout is shallow. The dev did the right thing: measured both halves instead of accepting or dismissing it.
I re-read the provision on
origin/mainrather than taking the report's word, and all three limbs hold:.github/workflows/lint.yml:151isfetch-depth: 0— and ⭐ its own inline comment states the reason unprompted: "A shallow clone has no merge base, and the check would degrade to 'not verified' on every run." The repo already knows this trap and already pays for the fix..github/workflows/lint.yml:3504isrun: pnpm check:published-files.- Both sit in the same job — key
lint:(:123),name: Lint & Repo Gates(:138). So the gate runs in the checkout that has the history.
⇒ The hazard is real in this container and absent in CI, which is where the clause runs. Assumption discharged, not waved through.
And the failure mode is closed the right way round. This is the gate's only history-dependent read, so it is the only place it could go vacuously green — a failed base read makes every package look born-sealed and the clause silently unanimous. Every such path is a refusal that names itself, carrying its own census control (
baseReadControl), the same shapeGATEDuses withEXPORTS_CENSUS_FLOOR. Proven end-to-end, not asserted:--basethat does not resolve ⇒ exit 1; an orphan-commit base with no merge base ⇒ exit 1. Absence is never a pass.⭐ The ablation found a real defect — that is the strongest thing in this delivery
An earlier draft accepted any pending changeset, and a genuine
./consoleremoval went green because an unrelated changeset already onmaincontained the string./consolein prose about a different release. With ~1300 changeset files of prose about these same packages, "some changeset mentions this path" is satisfied by accident constantly. Fixed by scoping the announcement to changesets this change adds or edits (79f3ef721).That is a gate that would have shipped looking correct and been answerable by coincidence — exactly the vacuous-green class #15410 measured this repo full of. It was caught because the ablation was real (5 legs, each mutation proven on disk by before/after counts, each restore proven byte-identical by
git hash-objectagainst the HEAD blob). Both batteries are registered inSELF_TEST_BATTERIESwith floors pinned at measured counts (26 and 31), so a case that stops running names itself. ⛔ Not the 179th vacuous self-test.The fifth cell — accepted, and it is faithful rather than scope creep
The ruling named four cells; the dev added a fifth:
package.jsonpresent at base butprivate/unnamed ⇒ born-sealed, passes. I judged this against the ruling's own rationale rather than its letter: B1 exists because a package born with a map seals nobody — there is no published predecessor whose consumers could be deep-importing it. A package npm never published has no published predecessor either, so gating the PR that first makes it public is the identical false positive B1 was chosen to avoid. It narrows the gate, it is pinned in the self-test, and it is declared in the PR body. Accepted.Also correct and worth naming: pattern keys are honoured in the resolution check, so widening a map to
./*is not counted as a removal, and the announcement is checked against the head map rather than for a form of words — writing@objectstack/cli/consolewhen./consolestill resolves does not satisfy it. That is the difference between the sentence #13662 needed and boilerplate.Size judged, not waved through
size/lon a card I dispatched as M. I checked the composition rather than the total: 786 added = 240 comment/doc + 42 blank + 504 code, across 13 new functions that each do a distinct required job — pattern-aware map resolution, the five-cell verdict, the changeset parser, a git base reader with four refusal paths, the changeset scoping the ablation forced, and the anti-vacuity control. The comment share matches this file's standing convention of writing the reasoning into the gate. Nothing here is unrequested, so this is not the "small feature, large expansion" REWORK case — the ruling itself asked for a four-cell discriminator plus fixtures for every cell plus a changeset requirement.skip-changesetis justified: the root manifest isprivateand no publishable package ships ascriptsentry in itsfileswhitelist — which this very gate enforces.⛔ My brief was wrong again on the same point, and this dev also caught it
I again told the dev to post its own
Claim:comment. AGENTS.md binds a PM-dispatched executor to post none, and the dev flagged the conflict rather than silently choosing. It is doubly moot here: my claim5555438431already names this dev's exact session ID and branch. Two independent devs caught the same defect in my brief in one round; it is fixed in this lane's template and was already withdrawn explicitly in the third dispatch of the round.Landing
CI at review time: 18 green, 11 skipped, 3 running (
Lint & Repo Gates,Type Check · workspace,Test Core (1/6)), 0 failing. ⛔ Not flipped ready and ⛔ not armed — enqueue eligibility is every check green, not the required subset. Flip at the next patrol onceLint & Repo Gatesconverges (~20–25 min from 23:50Z).⚠️ Note for the landing window: #15589 closes when this lands — its option A landed as PR #15717 and this is its entire remainder.
Generated by Claude Code
LANDED — PR #16082 merged
64011dd3fat 2026-09-06T01:22:25Z. The B1 ruling is inmain.domain:devx @ objectstackseat (#6023), sessionsession_01PU9zBGbH2s2ZtxSyu963M3, round 1. Probed on a re-fetchedorigin/mainviagit show origin/main:<path>.Probe — passes, with controls
limb reading on origin/mainANNOUNCEDinvariant present21 hits, at load-bearing sites: the invariant header :53, the why-it-reads-history block:109, the discriminator section:515, the announcement predicate:629, the base read + its control:671the five-cell verdict narrowingVerdict:571returns'born'|'retrofit'|'removal'|'unchanged'|'ungated-head'|'unreadable-base'cell 1a — absent at base ⇒ born :574the added fifth cell — private/unnamed at base ⇒ born:586(if (!base.name || base.private === true) return { kind: 'born' })refusal machinery mergeBaseOf:692,introducedChangesets:741,baseReadControl:771— all presentCONTROL 1 — grep reaches this file pre-existing GATED→ 21 hitsCONTROL 2 — grep can return zero nonsense term → 0file size 943 → 1726 lines, consistent with the PR's +786/−3 ⚠️ One probe I attempted, got a green from, and am throwing awayI ran
node scripts/check-published-files.mjsin the shared checkout and it exited 0 with a clean self-test. That reading is void and I am recording it rather than quietly dropping it, because the failure is instructive and it is the one this repo's own rule names:⛔ 不用共享检出的工作树核验 main,它的 HEAD 由别的 agent 摆布
The shared checkout's working tree is on
claude/pm-dispatch-devx-89idm4atc463d03e0— before the merge. Measured after the fact: on-disk file 943 lines, 0ANNOUNCEDhits;origin/main1726 lines, 21. So I executed the pre-merge gate and would have reported its green as evidence for the post-merge clause. What gave it away was the verdict text: the run printed the old sentence and the self-test named the old batteries, with no ANNOUNCED battery among them — the numbers disagreeing with the report is what forced the second look.⇒ Runtime verification of the landed clause is NOT MEASURED by this seat, and I am not substituting the textual probe for it. Running it soundly needs a checkout at
origin/mainwith an installed closure; doing it in this container would additionally compare against a working tree that predates the merge, which is worse than not measuring. CI is the arbiter: the clause runs inLint & Repo Gates(lint.yml:3504) inside the job that checks out atfetch-depth: 0(:151), and it ran green on this very PR before merge — that is the real runtime evidence, and it is the PR's, not this note's.What is now enforced
A package that had already published without an
exportsmap and gains one — or loses a subpath that previously resolved — must carry aminor-or-greater changeset naming deep specifiers that genuinely stop resolving, checked against the new map rather than for a form of words. Born-sealed packages (absent at base, or present-but-private) pass untouched, so the measured 44-of-51 false positives B-as-worded would have produced do not happen. Every failure of the base read is a refusal that names itself, never a silent pass.pm:dispatchedstripped and the assignee cleared in the same stroke; the card is closed by the merge (Fixes #15715).
Generated by Claude Code
- added a commit that references this issue
on Sep 9, 2026
Filed out of #15589. That card proposed three options; option A (a consumer-specifier ledger, held against the
exportsmaps) is implemented on branchclaude/issue-15589-consumer-specifier-ledger. This is option B, deliberately split off rather than landed with it. Unassigned; it needs a maintainer decision, not an implementation.The proposal, as #15589 words it
What B would have refused — measured
Measured on
a4421eb89, over everypackage.jsonthis repo publishes:exportsmap todaypackage.json)package.json)Method, so the numbers are re-derivable: for each publishable
package.jsondeclaringexports, the introducing commit is the oldest fromgit log -S'"exports"' -- PATH; the package counts as born-sealed when that commit equals the oldest--diff-filter=Acommit for the same path. Non-shallow checkout (git rev-parse --is-shallow-repository= false).Why that split IS the decision
A package born with an
exportsmap seals nobody — there is no published predecessor whose consumers could be deep-importing it. Every incident this class has produced came from the 13 retrofits, and in fact from one of the 7 commits:6a571d3f7(#13123), the seal that broke cloud'sobjectos-runtime(#13662) and hotcrm's hook-body harness (#15325).So B as worded fires on 51 commits to catch a class that lives in 7. For 44 of the 51 it would demand a
minorbump and aBREAKING-style consumer note naming the deep paths that stop resolving — for a package that had no consumers and no deep paths that stopped resolving, because it had never been published without the map. That is roughly 7:1 against, and the false positives are not evenly spread: they land on new-package PRs, which is exactly where a "declare who this breaks" demand is least likely to be read closely and most likely to be discharged with boilerplate. A gate answered by boilerplate 44 times out of 51 stops being read by the 7th time it matters.The 7 retrofit commits are also not uniformly the dangerous shape.
818e02700is titledfix(cli): print init's "Created files" summary after install, not before— anexportschange riding along on a commit about something else. That one is an argument for B: a discriminating gate would have made that rider announce itself.Options
package.jsonthat already existed in the merge base (and on subpath removal from an existing map). Cost: the discriminator is a second git query inside the gate, and it is history-dependent, which no othercheck:published-filesclause is. Catches all 7, waves through all 44.exportsmaps, and make "a published package declares one" a gate #13123, which introduced rather than removed.Recommendation
B1, and if the history dependence is judged too costly for
check:published-files, B3 over B2 — a rule that is silent on the ambiguous half beats one that cries wolf 44 times. B2 as literally worded should not be taken.Note what A does and does not already cover, since it changes B's value: A reds on the PR that seals a specifier a named, cited out-of-repo consumer imports. It is silent about a consumer nobody has written down. B is the half that does not depend on the ledger being complete, which is why declining it (B4) is a real position and not merely inaction.
Boundary
⛔ Not a request to widen any
exportsmap, and not a request to weakencheck:published-files's existing GATED invariant. Sealing was correct in both incidents; the gap is visibility, not policy.⛔ This card is not blocked on option A, and option A is not blocked on it.
Generated by Claude Code