Skip to content

feat(spec): declare the browser boundary — a browser export condition excluding the pg-grammar arm (#11072) - #11098

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-11072-spec-browser-condition
Aug 22, 2026
Merged

feat(spec): declare the browser boundary — a browser export condition excluding the pg-grammar arm (#11072)#11098
os-zhuang merged 2 commits into
mainfrom
claude/issue-11072-spec-browser-condition

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #11072

What this does — Option A, per the maintainer's 2026-08-22 ruling on the card

@objectstack/spec declares its browser boundary in the exports map. The five entries whose module graph reaches the driver-config validators — ., ./data, ./system, ./kernel, ./cloud — gain a browser export condition (both format halves, condition first in each entry object) pointing at a second build under dist/browser/** in which the postgres url refinement's pg-grammar arm is excluded. pg-connection-string — the parser pg itself uses, deliberately (standing #8082/#8337 grounds; Option C stays rejected) — statically resolves a filesystem require, so any browser bundler whose client graph reached one of these entries failed on an unresolvable fs (objectui's docs site, red on its main).

Re-measured on this base (4257e4e), not carried from the card: the card (at 3e26359) counted six poisoned entries; on this base ./shared no longer links the parser (full-file scan of both format halves, not head-of-file), so the poisoned set — and therefore the browser-conditioned set — is these five. Single call site re-confirmed: packages/spec/src/data/driver/postgres.zod.ts. Zero pre-existing browser conditions confirmed.

Mechanism

  • Seam module pair (src/data/driver/pg-url-grammar.server.ts / pg-url-grammar.browser.ts): the server twin carries the one pg-connection-string import and the full A postgres config.url that pg itself cannot parse passes the authoring door — the shared credential-free URL check deliberately never parses #9091 grammar refusals, verbatim; the browser twin answers no findings. postgres.zod.ts keeps the shape-only pre-parse checks (unix-socket short-circuit, fs-reading ?sslcert=/?sslkey=/?sslrootcert= refusal) in every build — exactly the degradation the ruling prescribes.
  • Build (tsup.config.ts): a second JS pass emits the five entries to dist/browser/** with an esbuild resolve hook (swapServerOnlyGrammarArm) that swaps the seam specifier for the browser twin. Keyed to the one audited seam, NOT a blanket alias of the package — a future direct import of the parser lands in the browser bundles and the gate refuses it at this producer instead of silently degrading it. The DTS pass and the Node JS outputs are untouched; browser.types reuses the main build's declarations (identical public API, check:api-surface unchanged). Naming note: the seam was first named .node.ts and tsup's built-in native-addon handling intercepts any specifier ending .node — hence .server.
  • Node-side behaviour: zero movement (the ruling's pinned requirement). Node's resolver never matches browser; every existing import/require condition points at the same files; the full DSN refusal still runs for Node consumers — held by the existing postgres.test.ts pins (all green) plus a new pg-url-grammar.test.ts pinning both twins' contract (server refuses multi-host / non-numeric port / scheme-less; browser answers no findings for the same values). Option B (server-only publish entry) not taken — rejected in the ruling.
  • The pin the card asked for (check:browser-reachable-entries, second rule): every bundle a browser bundler resolves — browser-conditioned or not — must link no Node builtin and no declared server-only package. A subpath without a browser condition is judged on its ordinary bundles, so the NEXT server-only dependency added to any entry reds this package's own CI instead of a downstream bundler hours later. Positive control: the scan must keep finding server-only links on the Node side of the browser-conditioned subpaths (currently 10 = 5 entries × 2 formats); zero is a refusal, never a silent green.

Verification (all at head f975d021, the tree this PR ships)

  • pnpm --filter @objectstack/spec test — verdict line: Tests 11066 passed (11066) (416 files; includes every A postgres config.url that pg itself cannot parse passes the authoring door — the shared credential-free URL check deliberately never parses #9091 postgres pin).
  • pnpm --filter @objectstack/spec typecheck — green; its check:test-typecheck verdict line reports the spec test-typecheck ledger held (55 files / 263 errors, unchanged).
  • pnpm --filter @objectstack/spec build && check:generated — verdict line: All 14 generated artifacts are up to date (api-surface regenerated nothing: the export surface is byte-unchanged).
  • check:browser-reachable-entries — self-test 25/25 including the new [seam→spec] @objectstack/spec@17.1.0 statically imports pg-connection-string from six of its sixteen entry points, with no browser export condition — objectui's site build is red on main #11072 cases, then the real audit: 44 bundle(s) scanned … 5 browser-conditioned subpath(s), 10 server-only link(s) on their Node side — positive control held.
  • Gate reverse-verification, two legs, both red in the expected direction, both restored (trap-guarded script; mutation proven on disk by anchored grep counts before/after, restore re-proven the same way): (leg A) a node:fs import appended to dist/browser/data/index.mjs → exit 1, verdict line names ./data linking the Node-only specifier node:fs; (leg B) ./data's browser condition removed from package.json while its Node bundle still links the parser → exit 1, verdict lines name both format halves linking pg-connection-string. No rebuild involved by design: the gate reads dist directly, so mutating dist/package.json is the lever the gate actually sees.
  • Empirical browser probe (the card's objectui repro was impractical from this worktree; an equivalent local probe was run instead, as the dispatch allows): esbuild platform: browser, resolving @objectstack/spec and @objectstack/spec/data through the exports map — bundles green, 3.27 MB output containing no parser import and no fs import; the grammar-refusal text is absent while the shape-only fs-param refusal text is present (proof the browser condition, not the Node file, was resolved). Positive control: bundling the Node-conditioned dist/data/index.mjs under the same platform fails with exactly the card's error — could not resolve fs.
  • Dispatch-gates-derived families (node scripts/pm/dispatch-gates.mjs, no hand-fed paths): all 25 path-matched families run at head — all exit 0 except two, both the same environmental precondition (66 of 67 workspace packages unbuilt in this fresh worktree, not a finding about this diff): check:doc-formula-expressions went green after building its one input (@objectstack/formula) — verdict line 9 @example(s) judged clean across 981 packages/spec/src files; check:dev-prereqs's own output says The workspace is not built — 1 unmet precondition, not a list of problems, and the half about this package passed (spec's build-input hash stamped by the build). Convention-triggered (new test file): check:query-options-erasure, check:type-check-coverage, check:engine-double-contract, check:where-matcher all green. Declared narrowing: check:type-check-debt --re-measure (full workspace) was not run locally — it requires the built closure of all 67 packages; the entries this diff can move are spec's, re-measured green inside typecheck above, and no other package's program inputs changed (spec's built declaration surface is proven unchanged by the api-surface artifact at this head). CI runs the full farm on this PR regardless.

Scope and adjacents

Generated by Claude Code


Generated by Claude Code

claude added 2 commits August 22, 2026 16:24
…xcluding the pg-grammar arm (#11072)

Option A per the maintainer's 2026-08-22 ruling: the five entries whose
graph reaches the driver-config validators (., ./data, ./system, ./kernel,
./cloud) gain a browser export condition pointing at dist/browser/ bundles
in which the postgres url refinement's pg-grammar arm (pg-connection-string,
whose parse statically resolves require('fs')) is swapped for a
dependency-free twin. Node-side resolution and behaviour are unchanged.

check:browser-reachable-entries grows a second rule: every bundle a browser
bundler resolves (browser-conditioned or not) must link no Node builtin and
no server-only package, with a positive control on the Node side.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 12 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/plugins/development.mdx (via outDir (symbol))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/README.md, packages/spec/package.json) — pages documenting those are invisible to this run
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 126 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 ab47f6974a275586355f06abd83a0975b8637d12packageMentionDocs.

Which tree this was computed on

This run read content/docs from fc1cb42a10cc8bc7d10dc55971b8bc5fc008742c — the merge of head f975d0211a803cc919f3e2291090f559d01a2a3e into base ab47f6974a275586355f06abd83a0975b8637d12, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fc1cb42a10cc8bc7d10dc55971b8bc5fc008742c && git checkout fc1cb42a10cc8bc7d10dc55971b8bc5fc008742c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab47f6974a275586355f06abd83a0975b8637d12 f975d0211a803cc919f3e2291090f559d01a2a3e && git checkout -B drift-repro ab47f6974a275586355f06abd83a0975b8637d12 && git merge --no-ff f975d0211a803cc919f3e2291090f559d01a2a3e

node scripts/docs-audit/affected-docs.mjs --json ab47f6974a275586355f06abd83a0975b8637d12

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ab47f6974a275586355f06abd83a0975b8637d12 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

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

    ✗ Build failed in 6.09s
    

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

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

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

历史信号:

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

分诊清单:

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

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

Merged via the queue into main with commit 189373b Aug 22, 2026
40 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-11072-spec-browser-condition branch August 22, 2026 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation protocol:data size/l tests tooling

Projects

None yet

2 participants