docs: make technical prose concrete

This commit is contained in:
Turtle
2026-08-10 16:34:20 +08:00
parent 673e7cddc5
commit a27efdef36
459 changed files with 1342 additions and 1329 deletions
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/settings/settings/README.md
README.md: 5bfbf4623c937c2b66886f71adf27075523f5d28
README.zh.md: 98808424ba1af2210067bd7f74baa6027decbb06
README.md: 7917f38017bfb23dc4718ee533c1f9a92b519d41
README.zh.md: f46cf433b4d2207b17b0f40cc3b9cf70794b512c
+1 -1
View File
@@ -11,7 +11,7 @@ User-settings Service Definition (`ctx.settings`). One provider holds a raw docu
- `register(ns, schema, { base?, applies? })` — returns the owner `SettingsScope` (`get`/`watch`/`update`). The registration is an effect on the calling plugin's fiber: disposing that fiber removes the namespace and its observers. A stored section the schema rejects fails the registration itself; a duplicate namespace fails loud.
- `describe(options?)` — one descriptor per namespace (`schema.toJSON()` envelope, resolved value, detached `base`/`user` layers, `applies`) for configuration surfaces; a field's presence in `user` is what marks it user-overridden. `describe({ redactSecrets: true })` strips `role('secret')` fields from every layer and adds the `secrets` slot list (`{ path, set }`); every wire surface MUST pass it, and the pure `redactSecrets(schema, value)` walker is exported for other wires.
- `get(ns)` — resolved value, `undefined` while unregistered.
- `update(ns, patch)` — deep-merges the plain-object patch into the user section only (never the `base`), validates the resolved candidate, persists through the provider, then commits. Patches must be JSON-shaped data: a Date, Map, BigInt, non-finite number, or circular reference rejects with its `$`-rooted path before anything persists (YAML/JSON storage would silently distort such values on reload). Validation failure rejects before anything is persisted; a read-only provider (`writable: false`) rejects every write. Writes to one namespace are serialized in call order.
- `update(ns, patch)` — deep-merges the plain-object patch into the user section only (never the `base`), validates the resolved candidate, persists through the provider, then commits. Patches may contain only JSON-compatible data: a Date, Map, BigInt, non-finite number, or circular reference rejects with its `$`-rooted path before anything persists (YAML/JSON storage would silently change such values on reload). Validation failure rejects before anything is persisted; a read-only provider (`writable: false`) rejects every write. Writes to one namespace are serialized in call order.
- `replace(ns, section)` — sets the user section wholesale: the deliberate reset (`replace({})` re-inherits `base` and schema defaults).
- `mutate(ns, ops)` — applies ordered `{ op: 'set' | 'unset', path }` edits to the section as it stands when the write reaches the front of the queue. This is the removal path for any caller holding an INCOMPLETE view: a configuration UI reads the redacted descriptor, so rebuilding a section from it and replacing wholesale deletes every secret the wire never returned, while an op names the one field it means.
- Every write takes an optional `expectedRevision`. Each descriptor carries the namespace's `revision`, a monotonic counter over its RAW section; a write whose expectation no longer matches rejects with `SettingsConflictError` (`code: 'SETTINGS_CONFLICT'`, both revisions attached) instead of overwriting the writer that landed first. The write queue orders writes but cannot by itself tell a fresh writer from one holding a stale snapshot.
+1 -1
View File
@@ -11,7 +11,7 @@
- `register(ns, schema, { base?, applies? })` — 返回 owner 的 `SettingsScope``get`/`watch`/`update`)。注册是调用方插件 fiber 上的 effectdispose 该 fiber 即移除 namespace 及其观察者。schema 拒绝的存量分节会使注册本身失败;重复 namespace 立即报错。
- `describe(options?)` — 每个 namespace 一条描述(`schema.toJSON()` 封装、解析值、分离出的 `base`/`user` 层、`applies`),供配置界面使用;字段出现在 `user` 中即标记其被用户覆盖。`describe({ redactSecrets: true })` 从每一层剥离 `role('secret')` 字段,并附加 `secrets` 槽位列表(`{ path, set }`);每个协议接口都必须传入它,纯遍历器 `redactSecrets(schema, value)` 已导出,供其他 wire 使用。
- `get(ns)` — 解析值;未注册时为 `undefined`
- `update(ns, patch)` — 把普通对象 patch 深合并进用户分节(绝不合并进 `base`),校验解析候选值,经提供方持久化后提交。patch 必须是 JSON 形状的数据:Date、Map、BigInt、非有限数或循环引用会在任何内容持久化前带着以 `$` 为根的路径拒绝(YAML/JSON 存储在重载时会静默扭曲这类值)。校验失败在持久化前拒绝;只读提供方(`writable: false`)拒绝一切写入。同一 namespace 的写入按调用顺序串行。
- `update(ns, patch)` — 把普通对象 patch 深合并进用户分节(绝不合并进 `base`),校验解析候选值,经提供方持久化后提交。patch 只能包含与 JSON 兼容的数据:Date、Map、BigInt、非有限数或循环引用会在任何内容持久化前带着以 `$` 为根的路径拒绝(YAML/JSON 存储在重载时会静默改变这类值)。校验失败在持久化前拒绝;只读提供方(`writable: false`)拒绝一切写入。同一 namespace 的写入按调用顺序串行。
- `replace(ns, section)` — 整体替换用户分节:这是刻意的重置(`replace({})` 重新继承 `base` 与 schema 默认值)。
- `mutate(ns, ops)` — 在写入排到队首那一刻的分节上,按序施加 `{ op: 'set' | 'unset', path }` 编辑。这是任何持有**不完整**视图的调用方的删除路径:配置 UI 读到的是脱敏后的 descriptor,据此重建分节再整体替换,会把 wire 从未回传的每个机密都删掉,而一条 op 只点名它真正要改的那个字段。
- 每次写入都可携带可选的 `expectedRevision`。每个 descriptor 都带有该 namespace 的 `revision`——一个针对其**原始**分节的单调计数器;期望值不再匹配的写入会以 `SettingsConflictError``code: 'SETTINGS_CONFLICT'`,并附上两个 revision)被拒绝,而不是覆盖先完成写入的写入方。写队列只保证写入的先后次序,它本身分辨不出新的写入方与持有陈旧快照的写入方。
+12 -12
View File
@@ -120,14 +120,14 @@ export interface SettingsScope<T> {
watch(callback: (next: T, prev: T) => void | Promise<void>): () => void
/**
* Merge a partial patch into this namespace's user layer and persist it.
* @param patch - plain-object patch over the user section; JSON-shaped data
* @param patch - plain-object patch over the user section; JSON-compatible data
* only (non-JSON values reject with their path before anything persists).
*/
update(patch: object): Promise<void>
/**
* Replace this namespace's user section wholesale; absent keys re-inherit
* the composition `base` and schema defaults (`replace({})` resets all).
* @param section - the complete next user section; JSON-shaped data only,
* @param section - the complete next user section; JSON-compatible data only,
* as for {@link update}.
*/
replace(section: object): Promise<void>
@@ -172,11 +172,11 @@ declare module 'cordis' {
}
/**
* Deep equality over JSON-shaped data (objects, arrays, primitives) — the
* Deep equality over JSON-compatible data (objects, arrays, primitives) — the
* Service Definition's single change-detection predicate, exported so the invariant
* companion checks exactly the implementation's relation.
* @param a - one JSON-shaped value.
* @param b - the other JSON-shaped value.
* @param a - one JSON-compatible value.
* @param b - the other JSON-compatible value.
* @returns whether the two values are structurally equal.
*/
export function deepEqualJson(a: unknown, b: unknown): boolean {
@@ -264,7 +264,7 @@ function applyPathOp(section: Record<string, unknown>, op: SettingsPathOp): Reco
return { ...section, [head]: applyPathOp(child, { ...op, path: rest }) }
}
/** Human label for a value rejected by the JSON-shape boundary (numbers reject inline). */
/** Human label for a value that lossless JSON cannot represent (numbers reject inline). */
function describeRejected(value: unknown): string {
if (value === undefined) return 'undefined'
if (typeof value === 'object' && value !== null) {
@@ -276,16 +276,16 @@ function describeRejected(value: unknown): string {
}
/**
* Detach one write input in a single walk that doubles as the durable-boundary
* shape check: only JSON data (plain objects, arrays, strings, finite numbers,
* Detach and validate one write input in a single walk before persistence:
* only JSON data (plain objects, arrays, strings, finite numbers,
* booleans, `null`) may reach a provider document. `structuredClone` alone
* would admit Dates, Maps, BigInts, and cycles that YAML/JSON storage then
* silently distorts on the reload round-trip. `undefined` entries in objects
* are skipped — the same sparse-patch semantics as {@link mergeLayers} — while
* an `undefined` array entry is rejected rather than coerced.
* @param root - plain-object write input (caller-checked).
* @param reject - builds the boundary error from a value label and its `$`-rooted path.
* @returns the detached JSON-shaped clone.
* @param reject - builds the validation error from a value label and its `$`-rooted path.
* @returns the detached JSON-compatible clone.
*/
function cloneJsonShaped(
root: Record<string, unknown>,
@@ -640,9 +640,9 @@ export abstract class Settings extends Service {
}
// Snapshot at call time: the queue must never read a caller-owned object
// the caller may keep mutating while the write waits its turn. The same
// walk is the JSON-shape boundary check (see cloneJsonShaped).
// walk rejects values that JSON cannot preserve (see cloneJsonShaped).
const snapshot = cloneJsonShaped(payload, (label, path) =>
new TypeError(`settings ${verb} for "${ns}" must be JSON-shaped data (found ${label} at ${path})`))
new TypeError(`settings ${verb} for "${ns}" must contain only JSON-compatible data (found ${label} at ${path})`))
const previous = this.writeQueues.get(ns) ?? Promise.resolve()
// Chain past a failed predecessor: one rejected write must not poison the
// namespace queue for every later caller.
@@ -479,11 +479,11 @@ describe('second review regressions', () => {
expect(applied).toEqual([1, 2])
})
it('rejects a function value as not JSON-shaped', async () => {
it('rejects a function value as not JSON-compatible', async () => {
const { ctx } = await boot()
const scope = ctx.settings.register(settingsNamespace('ui-theme'), ThemeSchema)
await expect(scope.update({ theme: () => 'dark' }))
.rejects.toThrow(/JSON-shaped.*function at \$\.theme/)
.rejects.toThrow(/JSON-compatible.*function at \$\.theme/)
})
it('rejects a write still queued when the service disposes', async () => {
@@ -616,7 +616,7 @@ describe('third review regressions', () => {
const { ctx, provider } = await boot()
const scope = ctx.settings.register(settingsNamespace('ui-theme'), z.object({ value: z.any() }))
await expect(scope.update({ value: { at: new Date(0) } }))
.rejects.toThrow(/JSON-shaped.*Date at \$\.value\.at/)
.rejects.toThrow(/JSON-compatible.*Date at \$\.value\.at/)
expect(provider.persisted).toEqual([])
})
@@ -914,10 +914,10 @@ describe('mutate (path-addressed writes)', () => {
expect(ctx.settings.describe().find(d => d.ns === KEYED)!.user).toEqual({ apiKey: 'sk-stored' })
})
it('rejects a value the JSON-shape boundary refuses', async () => {
it('rejects a value that lossless JSON cannot represent', async () => {
const ctx = await mounted({ keyed: {} })
await expect(ctx.settings.mutate(KEYED, [{ op: 'set', path: ['baseURL'], value: new Date() }]))
.rejects.toThrow(/must be JSON-shaped data/)
.rejects.toThrow(/must contain only JSON-compatible data/)
})
})