Files
trade-message-center/.trellis/spec/project/module-ownership.md
T

6.9 KiB
Raw Blame History

类型所有权与模块出口

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

// Wrongfeature/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.ts owns strict isPlainRecord and 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 isPlainRecord and preserve their existing fail-closed result (null, false, or stable protocol rejection).
  • apps/chrome-extension/src/lib/guards.ts owns loose isObjectRecord only 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-harness is 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 src path.

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 isPlainRecord before exact-shape decoding, while a MAIN-world SDK reader uses isObjectRecord before 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 isRecord and isPlainObject; only the documented harness inline strict implementation may be self-contained. Run strict typechecks and verify cross-package imports do not use src or dist paths.

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;