diff --git a/README.md b/README.md index d7b7a75ac..15c21f265 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Elements are the durable application contract; JSX and Basic Element are the authoring layer; Declarative Shadow DOM is the default server representation; interactive regions upgrade selectively. -Published package line: `0.42.0-alpha.8` (`v0.42.0-alpha.8`) — the stable five-package +Published package line: `0.42.0-alpha.9` (`v0.42.0-alpha.9`) — the stable five-package release under ADR-0119's scoped interface freeze; the abandoned beta naming is not an active line. diff --git a/README.zh.md b/README.zh.md index a88f95de7..111dc0ecb 100644 --- a/README.zh.md +++ b/README.zh.md @@ -6,7 +6,7 @@ Elements 是可长期保存的应用组件模型;JSX 与 Basic Element 是作者层; Declarative Shadow DOM 是默认服务端表示;交互区域按需升级。 -已发布包线为 `0.42.0-alpha.8`(`v0.42.0-alpha.8`)——ADR-0119 范围化接口冻结下的 +已发布包线为 `0.42.0-alpha.9`(`v0.42.0-alpha.9`)——ADR-0119 范围化接口冻结下的 stable 五包版本;已放弃的 beta 命名不再是当前版本线。 ## 当前产品 diff --git a/docs/audit/2026-07-30-packages-code-review.md b/docs/audit/2026-07-30-packages-code-review.md new file mode 100644 index 000000000..f2dc5d19b --- /dev/null +++ b/docs/audit/2026-07-30-packages-code-review.md @@ -0,0 +1,68 @@ +# 2026-07-30 packages/ Code Review (verified) + +> Scope: `packages/element`, `packages/app`, `packages/adapter-vite`, +> `packages/create`, `packages/ui` — source line `v0.42.0-alpha.8`, `main`.\ +> Method: external review report, then every finding re-verified against +> source (files, line numbers, and claims read directly).\ +> Disposition: all 11 findings filed as issues #632–#642, milestone +> `v0.42.0-alpha.9`. Plan: `docs/release/v0.42.0-alpha.9-plan.md`. + +## Overall verdict + +**Quality grade: B+ (good).** Clear layering across the five packages: +`element` = zero-dep runtime core, `app` = authoring API, `adapter-vite` = +build orchestration, `create` = scaffolding, `ui` = component library. +Verified: no `as any` and no TODO/FIXME anywhere in `packages/*/src`; +strong security posture (prototype-pollution guards, CSP nonce validation, +HTML escaping); ADR-referenced comments throughout. + +## Findings (verified) + +| ID | Sev (final) | Finding | Location | Issue | +| --- | ---------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| H-1 | **Medium** (downgraded, see below) | Module-level `dataStack` render context | `app/src/internal/router/data-context-store.ts` | [#632](https://github.com/open-element/openelement/issues/632) | +| H-2 | High | `escapeHtml` (map single-pass) vs `escapeAttr` (5 chained replaces) dual implementation | `element/src/internal/core/html-escape.ts:32-45` | [#633](https://github.com/open-element/openelement/issues/633) | +| M-1 | Medium | 1-line re-export layer `internal/router/internal/data-context.ts` | `app/src/internal/router/internal/data-context.ts` | [#634](https://github.com/open-element/openelement/issues/634) | +| M-2 | Medium | Per-package task matrix inconsistent (only `element` complete) | `packages/*/deno.json` | [#635](https://github.com/open-element/openelement/issues/635) | +| M-3 | Medium | Raw `console.error` in client-router/spa instead of `createLogger` | `app/src/internal/router/client-router.ts:377,380,487`; `app/src/spa.ts:64,204,211` | [#636](https://github.com/open-element/openelement/issues/636) | +| M-4 | Medium | `OpenButton._handleClick` ~50 lines, multiple responsibilities | `ui/src/open-button.tsx:232-281` | [#637](https://github.com/open-element/openelement/issues/637) | +| M-5 | Medium | Unused `export default` on all 10 UI components | `ui/src/*.tsx` | [#638](https://github.com/open-element/openelement/issues/638) | +| L-1 | Low | `_getLocale` reads `locale` via `as Record` cast | `element/src/open-element-implementation.ts:599` | [#639](https://github.com/open-element/openelement/issues/639) | +| L-2 | Low | Redundant Fragment string-compare branch — at **3 sites**, not 1 | `render-ir.ts:216`, `jsx-render-dom.ts:328`, `event-hydration.ts:86` | [#640](https://github.com/open-element/openelement/issues/640) | +| L-3 | Low | `create` has `__tests__/cli.test.ts` but no `test` task | `packages/create/deno.json` | [#641](https://github.com/open-element/openelement/issues/641) | +| L-4 | Low | Unknown `tagName` in SPA `renderComponent` fails silently (blank page) | `app/src/spa.ts:70-92` | [#642](https://github.com/open-element/openelement/issues/642) | + +## Verification corrections to the original report + +1. **H-1 downgraded High → Medium.** The claimed "concurrent SSR stack-frame + corruption" path does not exist: `authoring.ts:296-321` wraps + push → `definition.render()` → pop in a **synchronous try/finally window** + (`render()` is not async), so single-threaded JS cannot interleave two + requests inside it. Residual (real) risk is narrower: hooks called by + function components evaluated later in the async `renderToNode` phase may + read an empty/foreign frame. `AsyncLocalStorage` remains the right + long-term direction, as an architecture evolution item. +2. **M-2 risk wording softened.** Root `deno.json` `test` is a bare recursive + `deno test`, so `app`/`ui`/`create` tests DO run via the root task. The + gap is per-package DX and explicit CI invocation — not silent test loss. +3. **L-2 undercounted.** The redundant Fragment detection appears at 3 sites, + not 1. `Fragment = Symbol.for('openelement.fragment')` uses the global + symbol registry, so the string-compare branch is redundant absent a + cross-realm scenario. + +## Cross-cutting patterns + +| Pattern | Sites | Governance | +| --------------------------- | ---------------------------------------------------- | ------------ | +| Non-unified logging | app (console) vs element/adapter-vite (createLogger) | #636 | +| Dual escape implementations | escapeHtml / escapeAttr | #633 | +| UI default exports | 10 component files | #638 | +| Package task drift | 5 packages, 5 shapes | #635 (+#641) | + +## Dimensions not judged (missing evidence) + +- Actual per-package CI gate coverage (workflows drive tests via autoflow + tooling, not direct `deno task test` calls). +- npm publish consumer resolution of all subpath exports. +- Whether adapter-vite SSG rendering serializes requests (relevant to the + residual H-1 risk only). diff --git a/docs/audit/2026-07-30-team-review.md b/docs/audit/2026-07-30-team-review.md new file mode 100644 index 000000000..1a2681353 --- /dev/null +++ b/docs/audit/2026-07-30-team-review.md @@ -0,0 +1,87 @@ +# OpenElement packages/ 团队交叉审查报告(2026-07-30) + +> 本文档是 **团队版独立审查记录**,与 `2026-07-30-packages-code-review.md`(单轮核实版)互为补充。 +> 团队:主理人 齐活林(Delivery Director)|架构师 高见远|工程师 寇豆码|QA 严过关。 +> 审查性质:**仅审查,不修改任何文件**。三方各自独立读源码,结论经主理人汇总对照。 + +## 方法论与前提说明 + +- 上一轮(2026-07-30 单审)由主理人一人逐条核实,结论已写入 `2026-07-30-packages-code-review.md` 并发出 11 个 issue(milestone `v0.42.0-alpha.9`)。 +- 本轮由三位成员**独立**重读源码交叉验证,不以主理人结论为前提(仅作为对照锚点)。 +- 环境限制:本运行时未注册 `software-architect` / `software-engineer` / `software-qa-engineer` 自定义 agent 类型,spawn 阶段即失败。已用基础 `general-purpose` agent 等价替代,角色职责在 prompt 内显式指定,团队归属不变。结论有效性不受影响。 + +## 交叉审查结论总表 + +| 条目 | 单审结论 | 团队结论(三方) | 偏差 / 校正 | +| --------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| **H-1** dataStack 并发 SSR | 主路径不成立,High→Medium;残余=async 阶段读空帧 | 架构师:主路径**不成立**(同步 try/finally 内无 await 间隙);残余风险**重新定性为单请求作用域 bug**(嵌套非页面组件序列化期调用 `useLoaderData` 读空栈),**非并发竞态** | ✅ 方向一致,归因更精确:是作用域缺陷,不是并发 | +| **H-2** escape 双实现 | 属实,成立 | 工程师:属实但**非安全问题**(结果正确,仅实现不一致)→ **Low** | ⚠️ **严重度应降**:原报告/issue #633 标 High,团队独立判定 Low(维护性) | +| **M-1** 冗余间接层 | 属实 | 工程师:属实 Low | 一致 | +| **M-2** task 不一致 | 表格属实;"测试不会执行"表述过重 | QA:表格属实;根 `deno test` 无路径→递归发现,CI 门 `test:coverage:check` 同样递归,**不漏跑**;真正盲区在 M-4/L-4 边界 | ✅ 一致;措辞应改为"仅影响单包本地 `deno task test`" | +| **M-3** console.error | 属实 | 工程师:属实 Medium;补充"跨包不一致"(app 无自身 `createLogger`) | 一致 | +| **M-4** _handleClick 职责重 | 属实 | QA:属实中,提交路径难孤立测试;工程师:属实 Medium + 4 个额外异味 | 一致,且发现增量问题(见下) | +| **M-5** default export | 属实(原报 10 个组件) | 工程师:属实 Low,**实际 11 处** `export default`(`index.ts` 仅 named 导出,无消费者) | 数量校正:11 处 | +| **L-1** locale cast | 属实 | 工程师:属实(弱)Low | 一致 | +| **L-2** Fragment 冗余 | 补为 3 处 | 工程师:确认 **3 处**(`render-ir.ts:216` / `jsx-render-dom.ts:328` / `event-hydration.ts:86`) | 一致 | +| **L-3** create 缺 test task | 属实 | QA:属实 Low(`cli.test.ts` 179 行存在,deno.json 无 test task) | 一致 | +| **L-4** spa.ts tagName | 属实("静默失败") | 工程师:**描述需修正**——非法字符 `tagName` 会**抛 `SyntaxError`**(非静默);未注册自定义元素才静默空渲染;QA:边界未被覆盖是盲区 | ⚠️ 描述精确化:区分"非法字符抛错"与"未注册静默" | + +## 三方独立贡献 / 增量发现 + +### 架构师(高见远)—— 并发与进程级单例 + +- **F1** `dataStack` 跨请求交错覆盖:**主路径不成立**。证据:`authoring.ts:298` push → `definition.render` → `popData:319` 全在同步 `try/finally` 内;`callComponent` 同步执行,`await` 在求值之后,单线程无间隙可插入。 +- **F2** 残余风险重定性:嵌套"非页面"组件在序列化期调用 `useLoaderData` → 读空栈,属**单请求作用域缺陷**,非并发竞态。 +- **F3** `logger.ts` `_warned` 进程级 `Set`:`warnOnce` 一旦触发即**永久抑制**该进程内其他请求/SSG 多页同名告警 → Low。 +- **F4** `errors.ts` `_telemetryHook` 全局单例:若每请求 `setErrorTelemetryHook` 则 **last-writer-wins 竞态 + 跨租户泄漏** → Low(启动一次性)/ High(每请求 set)。建议改为请求级上下文。 +- 架构健康度:**B+**——依赖方向 `adapter-vite → element/app`、`app → element`、`ui → element` 单向无环,`element` 零依赖纯净。 + +### 工程师(寇豆码)—— 实现质量与新增异味 + +- H-2 / M-1 / M-3 / M-4 / M-5 / L-1 / L-2 / L-4 逐条核实见总表。 +- **M-4 关联 4 个额外代码异味**(均 `packages/ui/src/open-button.tsx` / `packages/app/src/spa.ts`): + 1. `spa.ts:78` `if (!route.tagName) return;` **死代码守卫不可达**——`RouteConfig.tagName` 必填(`client-router.ts:17`),`:72` 强转 `{tagName?: string}` 掩盖事实。 + 2. `open-button.tsx:178` `onClick={(e)=>this._handleClick(e)}` **多余箭头包装**;同文件 `:165` 锚点分支直接传引用,风格不一致。 + 3. `_handleClick` 在 `href`(锚点 ``)分支仍可能触发表单提交(若 `type="submit"` 则 `form.dispatchEvent`),**`` 上语义不当**。 + 4. `escapeHtml` 对 non-string 返回 `''`(`:33`),而 `escapeAttrValue` 经 `String()` 转换(`:48-50`)——**同包两种空值约定**。 + +### QA(严过关)—— 测试覆盖与边界 + +- 覆盖率门禁**真实存在**:阈值 lines 73 / branch 82 / func 77(`check-coverage.ts:73`、`deno.json:97`),scope = `packages/*/src` + `tools/lib`。 +- 根 `deno test` 与 CI 门均**无路径参数→递归发现**,`app/ui/create` 的 `__tests__/` 全部被跑入 → **M-2「测试不会被执行」不成立**。 +- 真正漏测盲区:**M-4 `_handleClick` 提交路径难孤立测试**、**L-4 `spa.ts` 无效 tagName / 空白页 / `applyPageHostData` 静默失败边界无覆盖**。建议保留门禁并补这两条路径单测,而非仅为包补 test task。 + +## 整体评级 + +**维持 B+**。三方独立结论一致支持单审的"报告代码事实基本属实、总体结论可信"判断,但严重度整体应**下调**: + +- H-1 由 High 降 Medium(且残余风险重新定性为作用域 bug); +- H-2 由 High 降 **Low**(维护性问题,非安全缺口); +- 其余 Medium/Low 与单审一致。 + +## 对 2026-07-29/30 已发 issue 的校正建议 + +| Issue | 当前 | 建议校正 | +| -------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| #632 H-1 | Medium(已含同步窗口证据) | 补一句:残余风险是**单请求作用域缺陷**而非并发竞态(采纳 F2) | +| #633 H-2 | **High / Gate 1 必须** | ⚠️ 降为 **Low**(团队独立判定非安全) | +| #634 M-1 | Low | 数量无关,保持 | +| #635 M-2 | Medium | 描述改为"单包本地 task 缺失,CI 不漏跑" | +| #636 M-3 | Medium | 补"跨包不一致" | +| #637 M-4 | Medium | 补 4 个关联异味(死代码守卫 / 多余箭头 / `` submit 语义 / 空值约定) | +| #638 M-5 | Low | 数量更正:11 处 `export default` | +| #639 L-1 | Low | 保持 | +| #640 L-2 | Low | 确认 3 处(补 `jsx-render-dom.ts:328`、`event-hydration.ts:86`) | +| #641 L-3 | Low | 保持 | +| #642 L-4 | Low | 描述精确化:非法字符抛 `SyntaxError` / 未注册静默空渲染 | +| — | — | **建议新增** F3 `_warned` 进程级单例(Low)、F4 `_telemetryHook` 全局单例竞态(Low/High)—— 可并入 #632 或单独开 issue | + +## 整改优先级重排(团队版) + +1. **必须(安全/正确性)**:F2 嵌套组件 `useLoaderData` 空帧作用域缺陷;F4 `_telemetryHook` 每请求 set 竞态(若确有此用法);L-4 未注册 tagName 静默空渲染无提示。 +2. **建议(一致性/可维护性)**:H-2 统一 escape;M-1 删冗余层;M-3 统一 createLogger;M-4 拆分 `_handleClick`;F3 `_warned` 进程级单例。 +3. **后续(清洁)**:M-5 去 default export(11 处);L-1 locale 类型;L-2 清 3 处 Fragment 冗余;L-3 create test task;M-2 补单包 task(非 CI 必需)。 + +--- + +_本记录由团队主理人汇总,三方独立结论为 source of truth;未修改任何源码。_ diff --git a/docs/current/VERSION_PLAN.md b/docs/current/VERSION_PLAN.md index 012883214..3128f0622 100644 --- a/docs/current/VERSION_PLAN.md +++ b/docs/current/VERSION_PLAN.md @@ -1,9 +1,9 @@ # v0.42.0 — WC Application Loop (light fullstack) release plan -> Current source package line: `v0.42.0-alpha.8`\ -> Current npm registry line: `v0.42.0-alpha.8`\ -> Next alpha train: `v0.42.0-alpha.8` (TP-5.7)\ -> Active release target: `v0.42.0-alpha.8`\ +> Current source package line: `v0.42.0-alpha.9`\ +> Current npm registry line: `v0.42.0-alpha.9`\ +> Next alpha train: `v0.42.0-alpha.9` (TP-5.7)\ +> Active release target: `v0.42.0-alpha.9`\ > Planning release target: `v0.42.0` (WC light fullstack / Application Loop)\ > Next release line: `v0.43.0` (Universal WC SSR)\ > Current maturity stage: stable (0.41.x line); 0.42.0 planned under ADR-0120 diff --git a/docs/governance/PROJECT_WORKFLOW.md b/docs/governance/PROJECT_WORKFLOW.md index 3d2e4e298..b6b63007d 100644 --- a/docs/governance/PROJECT_WORKFLOW.md +++ b/docs/governance/PROJECT_WORKFLOW.md @@ -11,8 +11,8 @@ complete because an issue, chat message, or SOP says it is complete. It is complete only when the repository contains the decision, the execution package, the implementation, and the gates that prove the claim. -Current execution anchor: published package line `v0.42.0-alpha.8`, active -train `v0.42.0-alpha.8` (TP-5.7), and `0.42.0` WC light fullstack planning +Current execution anchor: published package line `v0.42.0-alpha.9`, active +train `v0.42.0-alpha.9` (TP-5.7), and `0.42.0` WC light fullstack planning under ADR-0120 and `docs/current/VERSION_PLAN.md`. OpenElement is one Web Components-native, static-first application framework: Basic Element is an authoring mode, not a diff --git a/docs/release/v0.41.0-interface-snapshot.json b/docs/release/v0.41.0-interface-snapshot.json index d3290b7c1..14c6bf180 100644 --- a/docs/release/v0.41.0-interface-snapshot.json +++ b/docs/release/v0.41.0-interface-snapshot.json @@ -101,8 +101,9 @@ ] }, "./spa": { - "sha256": "79038615bb4d54805c3fac24440e620011b7ff3927302132cc1139484a965dfd", + "sha256": "5f55fc52ca8a0d5a64884349693d560d2d0cf8a8a85ee9efc67b1c323e859611", "publicDeclarations": [ + "export function assertValidTagName(tagName: string): void {", "export interface SpaAppOptions {", "export interface SpaAppInstance {", "export function defineApp(options: SpaAppOptions): SpaAppInstance {" @@ -185,56 +186,56 @@ "publicDeclarations": [] }, "./open-badge": { - "sha256": "557712982869455871b9b41474c698582c41769371d2e94f6347f51fc612f2d5", + "sha256": "91d157e29bea78e581acb46e7f287da5a6dfebd8f3fdbc6d68c24518de27b380", "publicDeclarations": [ "export const tagName = 'open-badge';", "export class OpenBadge extends OpenElement {" ] }, "./open-button": { - "sha256": "5e2ab4736e53b397c1336f0c069587d7edcc2ca6ea446b7bee518e3ed09761af", + "sha256": "63c629d06c7c666fcfc1fdd4024fe97895bb269fe57d75f8d919fc282ec111d7", "publicDeclarations": [ "export const tagName = 'open-button';", "export class OpenButton extends OpenElement {" ] }, "./open-callout": { - "sha256": "f667dabce2fbc4e719d43266a2ebd3c2d23add7c1088dfc9b8b49d4bb9812f6d", + "sha256": "77cd8141a1b888fa2b50f9d99088394312aa1db5996df0253dfd8b38931aea0b", "publicDeclarations": [ "export const tagName = 'open-callout';", "export class OpenCallout extends OpenElement {" ] }, "./open-card": { - "sha256": "3a4a9fde6d98124eb9833bbd58d8495efd26cf873c72245e750ef35431a5268a", + "sha256": "dbf9cc234da2a56eb177d039f084a5cd49b91131bc8646002b197e4c3d5225f1", "publicDeclarations": [ "export const tagName = 'open-card';", "export class OpenCard extends OpenElement {" ] }, "./open-code-block": { - "sha256": "61ab2fd8c49c15da94f11027319a3c7b91846970da75911676a963b1331e3c81", + "sha256": "28c16d6ebecfaff03e9b57870505eac66c60d8ba1863fc8cfdc081782e68f5c5", "publicDeclarations": [ "export const tagName = 'open-code-block';", "export class OpenCodeBlock extends OpenElement {" ] }, "./open-dialog": { - "sha256": "6ac1ac4f6b3364c5da6d79c327cbc3868f99a6d3708b9293073873cc5c9aa372", + "sha256": "d269b07cfe78e921df8c5225539912e1a2c06344087b1a94e7620c0b982556be", "publicDeclarations": [ "export const tagName = 'open-dialog';", "export class OpenDialog extends OpenElement {" ] }, "./open-dropdown": { - "sha256": "f313090f945a6f1f27960825c25f1dbc3137a8a95a4effdf579c89efe0ea1272", + "sha256": "f7b77f2d9d721184a3962c21c33e0f098e0c827d2e23dfbdb65d8a9da9ec17f9", "publicDeclarations": [ "export const tagName = 'open-dropdown';", "export class OpenDropdown extends OpenElement {" ] }, "./open-input": { - "sha256": "a85e5ffb970b7159e7c8a940f9c22e5ff2810d5bedec9de8d7f4ed8b6c5c7870", + "sha256": "7f8bc2182640bf8bcea6c413ae9ed4c476c7f01985bd416b73d2f88d6378f443", "publicDeclarations": [ "export const tagName = 'open-input';", "export class OpenInput extends OpenElement {" @@ -257,14 +258,14 @@ ] }, "./open-tabs": { - "sha256": "85fe927ef3e33b7eb8993e1572410f00cb21bcd07caf98a7fee1a40fb0bd3d06", + "sha256": "81e95beb24d760a9e29d0e7c9c99920d78a43a30a16e005d6a76a682ca6d9b8f", "publicDeclarations": [ "export const tagName = 'open-tabs';", "export class OpenTabs extends OpenElement {" ] }, "./open-theme-toggle": { - "sha256": "555523aa7fbc72097b71fde61b823ba2dc967bdb837a826f440f7fd307822708", + "sha256": "09711bf638a0f4fe33aab180c6d725c4d5ec2abae808488ba44fcb4611074014", "publicDeclarations": [ "export const tagName = 'open-theme-toggle';", "export class OpenThemeToggle extends OpenElement {" diff --git a/docs/release/v0.42.0-alpha.9-plan.md b/docs/release/v0.42.0-alpha.9-plan.md new file mode 100644 index 000000000..f746eb9ce --- /dev/null +++ b/docs/release/v0.42.0-alpha.9-plan.md @@ -0,0 +1,178 @@ +# v0.42.0-alpha.9 — packages/ 完整修复计划(可执行版) + +> Source train: follows `v0.42.0-alpha.8` (ADR-0114 alpha naming). +> Scope source: +> +> - 核实版审计 [2026-07-30-packages-code-review.md](../audit/2026-07-30-packages-code-review.md) +> - 团队交叉审查 [2026-07-30-team-review.md](../audit/2026-07-30-team-review.md)(13 条 finding 的源码事实与校正) +> - 本计划由架构师视角(高见远)生成逐 issue 任务卡,主理人整合。 +> Tracking: GitHub milestone [`v0.42.0-alpha.9`](https://github.com/open-element/openelement/milestone/16),**issues #632–#644(共 13)**。 +> Non-goal: 无新 product surface;本趟仅 quality/hygiene,不触及 ADR-0119 freeze surface 或 0.42 Application Loop scope (ADR-0120)。 + +## Objective + +将 2026-07-30 团队交叉审查的 13 条 finding 作为一趟 cleanup train 落地,使 `v0.42.0` RC 线从一致的日志、转义、task、导出面起步。团队审查已将两条原 High 降级(H-1 非并发、H-2 非安全),故本趟**无阻塞级安全/正确性问题**,按 Gate 2/3 推进即可。 + +## Gate 划分(反映团队降级结论) + +- **Gate 1 — must(blocking)**:无强制阻塞项。两个原 High 均已降级:H-1 主路径非并发(残余为单请求作用域 bug,归 Gate 2)、H-2 非安全(归 Gate 3)。 +- **Gate 2 — should fix(target alpha.9)**:#632 #633 #634 #635 #636 #637 #642 #643 #644 +- **Gate 3 — polish(0.42 线内任意趟)**:#638 #639 #640 #641 + +> 注:#644 严重度 Low/High 视用法(若确有每请求 `setErrorTelemetryHook` 则为 High),保守列入 Gate 2。 + +## 已锁定决策(2026-07-30 用户确认) + +团队 plan 原对 3 处留了 fork,本轮由用户拍板: + +| 决策点 | 选择 | 影响 | +| ----------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| #632/#644 request-scoped 上下文实现 | **显式传参**(render/hydrate 入口透传 request-scoped 上下文对象) | 不用 ALS/AsyncContext,Deno Deploy / Workers / Node 全兼容、无 unstable 依赖;代价是改部分内部函数签名 | +| #643 logger `_warned` | **渲染作用域**(彻底修) | 改为每次 render 作用域内的 Map/Set,非进程级文档化;保留 `resetWarnOnceForTests` | +| #636 app 日志 | **复用 `@openelement/element` 的 `createLogger`** | app 不本地封装,全仓单一日志实现 | + +## 逐 Issue 任务卡 + +**#634 M-1 (P2) 删冗余 re-export 层|风险 S|改动量 S(2)** + +- 文件:`app/src/internal/router/internal/data-context.ts`(删)、`app/src/authoring.ts` +- 改动:删除 `internal/data-context.ts`(仅 `export { popData,pushActionData,pushLoaderData } from '../data-context-store.ts'`);`authoring.ts:22` import 由 `./internal/router/internal/data-context.ts` 改为 `./internal/router/data-context-store.ts`。`index.ts:55` 走 `internal/router/data-context.ts`(另一文件)不受影响。 +- 依赖:无 +- 验证:`cd packages/app && deno test -A`;`grep -r "internal/data-context.ts" src` 无残留;`deno check src/authoring.ts` + +**#633 H-2 (P3, 已去 security) 统一 escape 实现|风险 S|改动量 S(1)** + +- 文件:`element/src/internal/core/html-escape.ts` +- 改动:`escapeHtml:32` 与 `escapeAttr:38` 共用同一 `ESCAPE_MAP` 单遍替换(`escapeAttr` 委托 `escapeHtml` 或共用回调)。**保留既有空值约定**(escapeHtml non-string→`''`,escapeAttrValue→`String()`),不在本 issue 归一(见 #637 异味④)。 +- 依赖:无 +- 验证:`cd packages/element && deno test -A`;新增回归测试断言两者对 `&<>"'` 输出一致。 + +**#632 H-1 (P2) render-scoped data context|风险 M|改动量 M(2-3)** + +- 文件:`app/src/internal/router/internal/data-context-store.ts`、`app/src/authoring.ts`(push/pop 调用) +- 改动:dataStack 改为每渲染请求作用域,通过 render/hydrate 入口**显式传参**透传 request-scoped 上下文对象(**已锁定:不用 ALS/AsyncContext**,全运行时兼容);修复嵌套非页面组件序列化期 `useLoaderData` 读空栈:render 入口建 request-scoped 栈,push/pop 操作它,dev 模式空栈读 `useLoaderData` 加断言警告。**非并发竞态**(团队审查 F2 归因:单请求作用域缺陷)。 +- 依赖:无(与 #634 同包,建议错开 authoring.ts) +- 验证:新增单测——render 中嵌套组件序列化期调 `useLoaderData` 拿到当前栈而非空;`deno test -A packages/app` + +**#638 M-5 (P3) 删 ui 11 处 default export|风险 S|改动量 S(~11)** + +- 文件:`ui/src` 下 11 个组件(含 `open-button.tsx:284`) +- 改动:删除各文件 `export default X`;`index.ts` 仅 named 导出(确认无消费者)。删 `open-button.tsx:284` +- 依赖:无 +- 验证:`cd packages/ui && deno test -A && deno lint`;`grep "export default" src` 为空 + +**#639 L-1 (P3) locale 属性|风险 S|改动量 S(2)** + +- 文件:`element/src/open-element-implementation.ts:599` + OpenElement 基类声明 +- 改动:基类声明 `locale?: string;`;`:599` 由 `(this as Record).locale` 改为 `this.locale` +- 依赖:无 +- 验证:`cd packages/element && deno test -A && deno check` + +**#640 L-2 (P3) 合并 Fragment 符号检测|风险 S|改动量 S(4)** + +- 文件:`element/src/internal/core/render-ir.ts:215-216`、`jsx-render-dom.ts:327-328`、`event-hydration.ts:84-86` +- 改动:抽 `isFragment(tag)` 辅助(置于 jsx-runtime.ts 或 fragment.ts),三处替换为 `isFragment(tag)`,移除冗余 `String(tag)==='Symbol(openelement.fragment)'` 分支 +- 依赖:无 +- 验证:`deno test -A packages/element`;Fragment 渲染/水合单测绿 + +**#635 M-2 (P2) 对齐 5 包 deno.json task|风险 M|改动量 M(5)** + +- 文件:`app/element/ui/adapter-vite/create` 各 `deno.json` +- 改动:按模板补齐 `build/test/lint/typecheck`(test=`deno test -A`,typecheck=`deno check`)。仅影响单包本地 `deno task`,CI 递归发现不漏跑(已确认) +- 依赖:无(先于 #641) +- 验证:各包 `deno task test/lint/typecheck` 绿 + +**#641 L-3 (P3) create 补 test task|风险 S|改动量 S(1)** + +- 文件:`create/deno.json` +- 改动:tasks 增 `"test": "deno test -A"`(`cli.test.ts` 已 179 行) +- 依赖:#635(模板来源) +- 验证:`cd packages/create && deno task test` + +**#636 M-3 (P2) 改用 createLogger|风险 S/M|改动量 S/M(2)** + +- 文件:`app/src/client-router.ts`、`app/src/spa.ts`(console.error:64,204,211) +- 改动:app 复用 `@openelement/element` 的 `createLogger`(**已锁定:不本地封装**);`spa.ts:64/204/211` 与 client-router.ts 的 `console.error` → `log.error`,统一日志前缀 +- 依赖:无(与 #642 同 PR) +- 验证:`deno test -A packages/app`;断言 loader/submit 失败走 logger + +**#642 L-4 (P3) spa.ts tagName 警告|风险 S|改动量 S(1)** + +- 文件:`app/src/spa.ts`(renderComponent:70-92;删 :78 死代码守卫) +- 改动:删 `spa.ts:78` 不可达 `if(!route.tagName) return;`(tagName 必填);createElement 前:非法字符(非小写字母数字`-`)抛带提示 `SyntaxError`;`customElements.get(tagName)` 未注册则 `log.warn('[spa] unregistered tagName: ')` 后空渲染 +- 依赖:#636(同文件) +- 验证:补全单测——非法字符抛友好 SyntaxError;未注册 tag 触发 warn 且空渲染 + +**#637 M-4 (P2) 拆分 _handleClick + 4 异味|风险 M|改动量 M(1 ui)** + +- 文件:`ui/src/open-button.tsx`(`_handleClick:232`、`onClick:165,178`、href 分支 :156-169) +- 改动:①拆 `_handleClick`(~50行)为 `_submitForm(form)`/`_resetForm(form)`,原方法仅分发。②异味② `:178` `onClick={(e)=>this._handleClick(e)}`→`onClick={this._handleClick}`(与 :165 一致)。③异味③ href `` 分支不触发表单提交(`_submitForm` 仅对 `