Skip to content

fix(devx): read a published README's import specifier against the packages that exist - #10923

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10893-readme-member-existence
Aug 22, 2026
Merged

fix(devx): read a published README's import specifier against the packages that exist#10923
os-zhuang merged 1 commit into
mainfrom
claude/issue-10893-readme-member-existence

Conversation

@claude

@claude claude Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #10893

Verified at e91070c149 — every gate quoted below was run on that exact commit.

The sweep found nine, not three

The card named three claims. A population-level sweep — every @objectstack/ token in all 60 published documents, checked against the workspace member map — found nine dead names across seven published documents, and the two worst were not on the card:

document named verdict
triggers/trigger-schedule/README.md (# title, ×4) @objectstack/plugin-trigger-schedule misnames itself
triggers/trigger-record-change/README.md (# title, ×2) @objectstack/plugin-trigger-record-change misnames itself
plugins/plugin-security/README.md @objectstack/plugin-org-scoping never existed
plugins/plugin-security/README.md @objectstack/service-tenant real, but ships from cloud
services/service-package/README.md @objectstack/service-marketplace never built
plugins/embedder-openai/README.md @objectstack/knowledge-turso not in this repo
drivers/driver-sqlite-wasm/README.md @objectstack/driver-sqlite, @objectstack/driver-postgres never existed
spec/prompts/architecture.md @objectstack/protocol never existed

Two packages tell readers to install themselves under a name that has never been published. packages/triggers/trigger-schedule ships as @objectstack/trigger-schedule; its README is titled # @objectstack/plugin-trigger-schedule and both of its fenced examples import from that name. Same for trigger-record-change. These are the pages npm renders for those two packages, and the exports gate has been green over them for its whole life.

Why the gate could not see any of it

check:published-readme-exports resolves a documented import through the workspace member mappublishedDocs() builds byName from workspaceDirs(), and run()'s resolveTarget opened with if (!byName.has(name)) return null. analyzeDocument reads null as "not this repo's business" and continues, which is the rule written for react.

So the chain is: member map → no entry → nullcontinue → no further reading. The subpath check, the type-entry check and the symbol check all sit below that line and never ran. The gate was strict about a member that exists and silent about one that does not — the inverse of the useful direction, and a strictly worse defect than the fabricated symbol it was built for (#9532), because a fabricated symbol at least installs.

The asymmetry was invisible in the output too. The header printed two numbers about what resolved and none about what it could not place:

214 import statement(s), 49 workspace type entr(ies).

What changed, and where the fence is

preTypeTarget(name, isMember) — exported, three arms — now owns the pre-type decision: a member defers to type resolution, an @objectstack/-scoped non-member is a finding, anything else is skipped exactly as before. The header states the population as a resolved/total pair, following check-published-readme-links' assertion 5, so a recogniser that stops matching shows up as a denominator that fell rather than as a defect count that never moved:

✓ check:published-readme-exports — 60 published document(s) across 77 workspace package(s); 212 import statement(s), 51 workspace type entr(ies), 197/197 @objectstack/ specifier(s) naming a workspace member.

The fence is the scope, and it was measured, not argued. Of 214 import statements, 200 are @objectstack/-scoped and 5 named no workspace member — 4 documents, 4 packages, every one a real defect. Zero false positives.

The wider fence — every @objectstack/ token anywhere in a published document — was measured too, and it is the naive version this file's header warns about: 14 distinct non-member names across 26 sites, of which five are legitimate by construction and would each need muting. @objectstack/security-enterprise (the enterprise edition, whose install hint packages/cli prints and capability-preflight.test.ts:74 pins), @objectstack/service-tenant (the cloud runtime, so named in packages/spec/src/system/constants/platform-object-names.ts:132), @objectstack/framework (the umbrella install name), and the two package names service-datasource's README recalls as its own past. Prose may name a package this repo does not build; a runnable import may not. That is the same boundary the existing diff-fence rule draws ("a removed import is a statement about the past").

No allowlist, deliberately. A roster of out-of-repo @objectstack/ packages is today a population of zero — no published document imports one from a fence — and this file refuses populations of zero everywhere else. Whether a runnable example may ever name one is a policy question; it is #10893's second shape and is filed as #10921 rather than answered with a blank file a future author fills in to quiet a red gate.

Is this a widening of #10368's fence?

No, and the distinction is the card's own. #10368 forbids widening what the gate reads about a resolved member — it is a hand-read, not a recognizer change. This adds a new population (specifiers that resolve to nothing), which #10893 says explicitly is why it was not folded into that card. Nothing about the member/symbol/call-site reads changed: symbolChecks and callChecks are unmoved except for the two import lines the repairs deleted.

The population refusal

scopeRefusal is a separate exported post-pass, modelled on bindingRefusal rather than bolted onto populationRefusal's axis. It has to be separate: targets counts specifiers that resolved, and this half's whole subject is the ones that did not — a scope recogniser that broke would drive ownScope to zero while targets stayed non-zero on the workspace members that carry no scope (create-objectstack, objectstack-blank).

What each dead claim now says, with the evidence

⛔ No replacement package name is invented anywhere below. Where the honest answer is "not available in this repo", the page says that.

  • plugin-org-scoping → the capability is real and ships elsewhere. packages/cli/src/commands/serve.ts:2379-2449 resolves @objectstack/organizations from the host app and constructs orgMod.OrganizationsPlugin(); packages/verify/src/harness.ts:466 records it as "cloud-private and only ever lives in the host app's node_modules"; doctor.ts:535 calls it "the enterprise @objectstack/organizations runtime". The README now names that package and its OrganizationsPlugin, and records that asking for the wall without it is a refusal to boot (ADR-0093 D5), not a silent downgrade. The fenced import { OrgScopingPlugin } block is gone rather than re-pointed: a reader cannot pnpm add a cloud-private package, so a runnable fence would still be a lie.
  • service-tenant → real, cloud, and not what that bullet was about. The "enforcing tenant isolation" bullet pointed at it; platform-object-names.ts:132 shows it contributes cloud control-plane objects, not the isolation wall. The bullet now points at the organizations runtime, which is where the wall comes from.
  • service-marketplace → never built. ADR-0003 lists it under "future", ADR-0016:82 and ADR-0025:94 both say "(separate service-marketplace)" as out of scope. The loading half that exists here is @objectstack/cloud-connection's MarketplaceInstallLocalPlugin, which installs a marketplace package into the local kernel through the manifest service.
  • plugin-trigger-schedule / plugin-trigger-record-change → a rename, pinned. The exported class names (ScheduleTriggerPlugin, TimeRelativeTriggerPlugin, RecordChangeTriggerPlugin) were correct all along; only the package name was wrong, and each package's own name field pins the right one.
  • knowledge-turso → the honest answer is a gap, and the page says so. The fenced block imported KnowledgeTursoPlugin and passed it an embedding option — the paste-and-run hazard the card names. There is no substitute: KnowledgeMemoryPluginOptions is { id?: string } and KnowledgeRagflowAdapterOptions has no embedder field, so no adapter in this repo consumes an IEmbedder at all. The example is now the embed() surface that does exist, and the missing half is stated. The runtime message that tells operators to mount that package is a different surface — filed as [finding] The knowledge settings manifest tells an operator at runtime to mount @objectstack/knowledge-turso, a package in no directory of this repo #10920.
  • driver-sqlite / driver-postgres → one package covers both. @objectstack/driver-sql's own README says "Supports PostgreSQL, MySQL, SQLite via Knex.js", with better-sqlite3 optional and pg an optional peer. The comparison table names it twice, and says why.
  • @objectstack/protocol@objectstack/spec. A published AI-prompt document instructing generators to write import { User } from '@objectstack/protocol') — the same sentence names packages/spec as the path it is replacing.

Two ablations, each with the prediction written down before the run

1. Do the repairs close findings the new assertion makes? Reverted the seven documents to origin/main, gate change in place. Predicted: exit 1, exactly 5 findings, all ids |specifier|, at five named file:line pairs, and zero findings naming the ten prose-only names. Observed: ✗ check:published-readme-exports — 5 undocumented symbol claim(s), all five |specifier|, at exactly embedder-openai:84, plugin-security:43, trigger-record-change:77, trigger-schedule:51 and :113, and grep -c over the ten forbidden names in the output returned 0.

No rebuild was needed for this leg and that is stated rather than assumed: the mutation is markdown only, dist/ is identical between legs, and the .d.ts side of every claim is untouched. Mutation proven on disk by marker counts anchored on the reverted text (from '@objectstack/plugin-trigger- 0→3, from '@objectstack/plugin-org-scoping' 0→1, from '@objectstack/knowledge-turso' 0→1), not by an editor's exit code. Restore leg proven with git diff --quiet — clean — and the same three counts back at 0.

The header prediction (194/199) was not observable on the red path, because header is only printed by the green branch. Reported rather than quietly dropped, and measured directly instead: reachedTargets on the reverted tree returns ownScope 199, unresolvable 5, rendering 214 import statement(s), 49 workspace type entr(ies), 194/199 — which also reproduces the exact 214 / 49 pair the card quotes from cc222637a3.

2. Are the new self-test pins load-bearing? Deleted the if (target.unresolvable) branch from analyzeDocument (545 chars, replaced with a marker; if (target.unresolvable) 1→0, marker 0→1).

First prediction: 7 failures. Observed 6 — and the miss was the useful part. The pin must name the package it could not place stayed green with the branch deleted, because ablation does not make the gate fall silent: {unresolvable:true} carries no declared, so the next arm claims it and emits a subpath finding whose text also interpolates the specifier. Reading findings[0] made that pin non-discriminating.

Fixed by selecting the finding by kind rather than by position, and the measured reason is recorded in the source beside it. Re-run with the corrected prediction of 7: observed ✗ check:published-readme-exports --self-test — 7 failure(s), matching pin for pin — the id pin, all five text pins, and the ordering pin flipping from ['specifier'] to ['subpath']. Every other pin stayed green, including the three preTypeTarget arms, both foreign negative controls, reachedTargets, scopeRefusal in both directions, headerLine, and the "repaired specifier is silent" control. Restored, git diff --quiet clean, self-test exit 0.

That ablation also turned the ordering pin from an argument into a measurement: without the branch, the author is told that a package which is not there fails to declare a subpath — a wrong remedy, not a missing one.

The negative control that decides whether this is usable at all

react, hono, zod, @libsql/client and @oclif/core are all imported by published READMEs in this tree and none is a workspace member. A scope-blind recogniser would redden five correct documents on its first run. Pinned as preTypeTarget('react', false) === null and preTypeTarget('@libsql/client', false) === null, plus a prefix-not-substring pin.

Does this gate refuse a baseline?

No — and the card's dispatch assumed it did. Its sibling check:published-readme-links refuses one by design; this one has scripts/published-readme-exports.baseline.json, a shrink-only ⛔ MAINTAINER-ONLY ledger reconciled in both directions. It currently holds entries: [], which is its documented success state. That is why all nine claims are repaired here and nothing is baselined: an author may not add a row, and a new fabrication must arrive as a fresh finding.

Changeset

Owed, and derived rather than recalled. All seven packages have private unset and a files array carrying the edited document — README.md for six, and packages/spec publishes prompts/, which is where architecture.md lives. A published README ships inside the npm tarball, so these edits reach an npm reader. That is the same conclusion PR #10892 reached for the same files and the precedent #9906 and PR #10365 set. patch for all seven; skip-changeset would have been wrong.

Coordination (#10840)

Re-checked immediately before pushing, against all 17 open PRs. #10906 is #10840's remedy and touches check-driver-conformance.mjs, check-skill-compatibility-version.mjs, check-skill-frame-sync.mjs, scripts/pm/bare-root-worklist.mjs and lint.ymlnot check-published-readme-exports.mjs. No collision, and none is likely: this gate derives its population from pnpm-workspace.yaml rather than from a bare-root literal, so it is not in that species, and #10906's TRIAGE map does not list it. No open PR touches any of the seven documents either, and origin/main moved to 58563be069 with no overlap on my paths.

Gates

25 run at e91070c149, exit codes captured before any pipe (cmd >> "$OUT" 2>&1; ec=$?). 22 derived by node scripts/pm/dispatch-gates.mjs with no path list passed — it takes its own change set from the merge base — plus check:i18n (convention-triggered by the plugin-security edit), check:nul-bytes, and check:published-readme-links (the sibling gate in this family, and the one whose assertion 5 reads relative targets in the files edited here). All 25 EXIT=0.

The two the card asks to be quoted, in their own words:

  • ✓ check:published-readme-exports — 60 published document(s) across 77 workspace package(s); 212 import statement(s), 51 workspace type entr(ies), 197/197 @objectstack/ specifier(s) naming a workspace member.
  • ✓ check:published-readme-links — 175 outbound link(s) across 60 published markdown file(s): 0 root-relative, 0 non-canonical origin(s), 27 docs-site page(s) resolved (0 via redirect), 1 anchor(s) verified, 102/102 relative target(s) found in the tree.

Both self-tests too: ✓ check:published-readme-exports --self-test and ✓ check:published-readme-links --self-test. Also check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys), check-nul-bytes: OK (scanned 6302 text file(s) … no raw ASCII control bytes) and ✓ check-nul-bytes --self-test: 75 assertions over a temp git repo (real scan() path). The remaining 20 are in the branch's run log, each with its own EXIT=0.

check:published-readme-exports needs a built workspace (it reads dist/*.d.ts), so pnpm build ran first — 71/71 tasks successful — under the shared verify lock. Every heavy command in this task went through scripts/pm/os-verify-lock.sh.

Filed, not fixed here


Generated by Claude Code

…kages that exist

Nine `@objectstack/` names across seven published documents named a package in no
directory of this repo; five sat on import lines inside runnable fences.
`check:published-readme-exports` was blind to all of them by construction — it
resolves a documented import through the workspace member map, so a specifier that
is not a member has no type entry to compare against and the gate reads no further.

The gate now makes the member-existence claim before the symbol one, fenced to this
repo's own npm scope, and the run header prints the scoped population as N/N.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 7 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 7 changed file(s) yielded no anchor (packages/drivers/driver-sqlite-wasm/README.md, packages/plugins/embedder-openai/README.md, packages/plugins/plugin-security/README.md, …) — pages documenting those are invisible to this run

Coarse fallback — 131 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 58563be069a70d09c4d9cc5f522bb27c5f8a78b2packageMentionDocs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Aug 21, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 22, 2026 03:55
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 22, 2026
Merged via the queue into main with commit 6ceaa4b Aug 22, 2026
33 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10893-readme-member-existence branch August 22, 2026 04:12
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32550303960 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 6.07s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️ 本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 74 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentation Improvements or additions to documentation size/l tooling

Projects

None yet

2 participants