mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
6.9 KiB
6.9 KiB
类型所有权与模块出口
1. Scope / Trigger
新增或移动领域类型、修改导出函数签名、增加 index.ts facade、调整导入路径或评审 re-export 时应用本规范。
2. Signatures / Roles
类型由表达概念的最近共同所有者定义;使用模块通过 import type 引用。index.ts 只有在承担稳定公共 facade 且有真实消费者时才 re-export。
// context/model.ts:唯一所有者
export type Context = { id: string };
// context/feature/index.ts:只使用,不转发
import type { Context } from "../model.ts";
export const runFeature = (context: Context): void => { void context; };
// package/index.ts:约定的公共 facade 才集中转发
export type { Context } from "./context/model.ts";
3. Contracts
类型定义位置与模块对外出口是两个不同问题。类型出现在函数参数/返回值中,不自动要求使用模块再次导出。添加或保留 re-export 前必须搜索实际消费者并确定 canonical import path;无公共边界或消费者时删除无意义转发。功能子模块不得机械级联转发依赖类型。
4. Validation & Error Matrix
| 发现 | 处理 |
|---|---|
| 类型由最近共同所有者定义,使用模块仅在签名引用 | 保留唯一所有者,使用 import type |
| 有明确公共 facade 且有真实消费者 | 仅在指定 facade 集中 re-export |
| re-export 无公共边界或实际消费者 | 删除转发,从 canonical path 导入 |
| 多个等价类型定义/导入入口 | 合并为一个所有者和 canonical path |
5. Good / Base / Bad Cases
- Good:共享
Context由上下文model.ts定义,功能只类型导入。 - Base:函数签名引用外部类型但没有公共入口需求,保留唯一所有者路径。
- Good:确有稳定包入口时,仅包级 facade 转发。
- Bad:每个功能
index.ts都 re-export 共享类型,制造多个入口。
6. Tests Required
- 修改类型定义或出口后搜索全部导入方,确认只有一个规范所有者。
- 对指定 facade 检查真实消费者和稳定路径;功能模块不得机械转发。
- 做严格类型检查,并检查无跨包内部引用、反向依赖或循环依赖。
7. Wrong vs Correct
// Wrong:feature/index.ts 仅因签名引用类型就重复提供入口。
import type { Context } from "../model.ts";
export const runFeature = (context: Context): void => { void context; };
export type { Context } from "../model.ts";
// Correct:功能只引用;只有约定的 package facade 才转发。
import type { Context } from "../model.ts";
export const runFeature = (context: Context): void => { void context; };
// package/index.ts: export type { Context } from "./context/model.ts";
Scenario: Shared protocol decoder guards and raw-host probes
1. Scope / Trigger
When a runtime guard is used by a shared protocol decoder, package configuration,
browser bridge, or an external host-object adapter, decide whether it validates a
canonical record or merely permits inspection of a raw host object. Do not retain
the ambiguous name isRecord for either meaning.
2. Signatures
// @trade-message-center/onetalk-contract public facade
export const isPlainRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
// chrome-extension internal raw-adapter helper
export const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
3. Contracts
packages/onetalk-contract/src/guards.tsowns strictisPlainRecordand the contract root facade is the only cross-package import path. It accepts current-realm plain objects and null-prototype objects, and rejects arrays, primitives, class/custom-prototype values, and cross-realm ordinary objects.- Contract decoders, server cursors/config, harness Node HTTP decoding,
canonical page-bridge envelopes, Chrome structured-clone messages and
extension configuration must use
isPlainRecordand preserve their existing fail-closed result (null,false, or stable protocol rejection). apps/chrome-extension/src/lib/guards.tsowns looseisObjectRecordonly for MAIN-world SDK, DOM, Event, WebSocket, React-like, and diagnostic probes before normalization. These callers may read a custom-prototype host object, but must normalize and whitelist before it crosses a canonical boundary.- The browser script embedded in
mind-test-harnessis the sole permitted self-contained strict implementation because it executes outside the Node module graph. It must have the same semantics and a comment naming that isolation reason. - Never add a compatibility alias or import a workspace package's
srcpath.
4. Validation & Error Matrix
| Boundary/value | Required result |
|---|---|
plain object or Object.create(null) at a strict boundary |
decode normally |
| array, primitive, class/custom prototype, cross-realm object at a strict boundary | existing fail-closed result |
| custom-prototype DOM/SDK/Event-like object in a raw MAIN-world adapter | loose probe may inspect and normalize it |
| raw object proposed for a Port, Bright frame, config, cursor, or persisted model | normalize first; never forward raw value |
| another package needs strict guard | import from @trade-message-center/onetalk-contract root |
5. Good / Base / Bad Cases
- Good: page bridge uses
isPlainRecordbefore exact-shape decoding, while a MAIN-world SDK reader usesisObjectRecordbefore selecting whitelisted fields. - Base: a package-local decoder imports the strict owner with a relative import.
- Bad: a server config parser keeps a local loose
isRecord, or a raw adapter receives a class instance and silently forwards it as canonical payload.
6. Tests Required
- Cover both guards with plain, null-prototype, array, null, primitive, custom-prototype, class-instance, and cross-realm values.
- Add representative nested strict-decoder coverage for frame/content/message payloads and each changed config/cursor/page-bridge boundary.
- Prove a raw host adapter still handles a custom-prototype SDK/Event-like value without allowing it through a canonical decoder.
- Search runtime source for
isRecordandisPlainObject; only the documented harness inline strict implementation may be self-contained. Run strict typechecks and verify cross-package imports do not usesrcordistpaths.
7. Wrong vs Correct
// Wrong: a canonical frame accepts any host object.
if (!isObjectRecord(frame)) return null;
// Correct: canonical input is strict; raw adapters remain explicitly loose.
if (!isPlainRecord(frame)) return null;
if (!isObjectRecord(rawSdkEvent)) return null;