fix(settings): harden seam and provider per review findings
Confirmed and fixed, each with a regression test that failed first:
- Concurrent update() lost patches (merge over one stale snapshot):
per-namespace serialized write queues; a failed write cannot poison
the queue for later writers.
- Fixed-name .tmp write followed planted symlinks and kept stale modes:
random-suffix sibling, exclusive-create (wx), 0600, cleanup on
failure, then rename.
- A throwing settings/updated listener escaped commit and permanently
wedged the provider reload chain (rejected refreshTask): commit now
contains listener failures (INVARIANT-coded errors still propagate),
async watcher rejections are adopted and contained
(watch callbacks are officially void | Promise<void>), and the
provider chains refreshes on a settled tail with an error log.
- No way to remove a user override: scope/service replace(section)
sets the user section wholesale; replace({}) re-inherits base and
schema defaults.
- The three-primitive provider contract did not hold (base never
called load()): the base Service.init loads and publishes once;
settings-local delegates via yield* super[Service.init]().
- Dispose did not quiesce: teardown flags closed, closes the watcher,
then awaits queued/in-flight reloads; closed is re-checked across
await points.
- Invariant now checks the authoritative relation with the seam's own
deepEqualJson: emitted next must equal settings.get(ns), and
next/prev must differ structurally (cosmokit dependency dropped).
- New docs/core-data-structures/settings.{md,zh.md} with type-equiv
blocks + manifest entries; catalog types moved from exemptions to
LINK_MAP; website page registered.
Both packages stay at per-file 100% coverage.
2026-07-28 18:18:34 +08:00
|
|
|
|
# 用户设置
|
|
|
|
|
|
|
|
|
|
|
|
[English](settings.md) | 中文
|
|
|
|
|
|
|
|
|
|
|
|
[dsh-settings](../../packages/settings/settings) 的用户设置 seam 持有一份按 namespace 分节的用户文档,并把每个已注册 namespace 解析为:schema 默认值,然后注册方的组合 `base`,最后用户分节。[dsh-settings-local](../../packages/settings/settings-local) 这类 provider 存储原始文档并推送外部编辑;消费插件注册 schema 后读取或观察解析值。组合配置仍留在 `cordis.yml`——namespace 只承载用户可编辑子集。
|
|
|
|
|
|
|
|
|
|
|
|
Source: [`packages/settings/settings/src/index.ts`](../../packages/settings/settings/src/index.ts)
|
|
|
|
|
|
|
|
|
|
|
|
## 标识
|
|
|
|
|
|
|
|
|
|
|
|
namespace 命名用户文档中一个插件所有的分节。brand 使其不与其他跨边界 id 混用;构造时校验小写 kebab-case 形态。
|
|
|
|
|
|
|
|
|
|
|
|
```ts type-equiv
|
|
|
|
|
|
/** Nominal id of one registered settings namespace. */
|
|
|
|
|
|
type SettingsNamespace = Branded<'SettingsNamespace'>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 注册
|
|
|
|
|
|
|
|
|
|
|
|
注册把 schemastery schema 绑定到调用方插件 fiber 上的 namespace——dispose 该 fiber 即移除 namespace 及其观察者。options 携带组合层与 owner 的生效时机。
|
|
|
|
|
|
|
|
|
|
|
|
```ts type-equiv
|
|
|
|
|
|
/** Registration options beyond the namespace schema. */
|
|
|
|
|
|
interface SettingsRegisterOptions<T> {
|
|
|
|
|
|
/** Composition-layer values resolved below the user layer (entry-config subset). */
|
|
|
|
|
|
base?: Partial<T>
|
|
|
|
|
|
/** Owner's effect timing, surfaced to configuration UIs; defaults to `live`. */
|
|
|
|
|
|
applies?: SettingsApplies
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`applies` 是 UI 提示而非机制:`restart` 的 owner 只是从不 watch,其值在构造期读取一次,配置界面可为待生效变更加标。
|
|
|
|
|
|
|
|
|
|
|
|
```ts type-equiv
|
|
|
|
|
|
/** When a namespace's changes take effect for its owner. */
|
|
|
|
|
|
type SettingsApplies = 'live' | 'restart'
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Owner scope
|
|
|
|
|
|
|
|
|
|
|
|
scope 是面向 owner 的句柄。`update` 把稀疏 patch 只合并进用户分节(绝不进 `base`);`replace` 整体替换分节,是删除/重置路径——替换中缺席的键重新继承 `base` 与 schema 默认值。同一 namespace 的写入按调用顺序串行,解析值是深冻结快照。
|
|
|
|
|
|
|
|
|
|
|
|
```ts type-equiv
|
|
|
|
|
|
/** Owner-facing handle for one registered namespace. */
|
|
|
|
|
|
interface SettingsScope<T> {
|
|
|
|
|
|
/** Current resolved value: schema defaults, then `base`, then the user layer. */
|
|
|
|
|
|
get(): T
|
|
|
|
|
|
/**
|
2026-07-29 10:07:28 +08:00
|
|
|
|
* Observe committed changes to this namespace's resolved value. Invocations
|
|
|
|
|
|
* of one callback run asynchronously, one at a time, in commit order; a
|
docs(settings): third-review contracts across READMEs, catalogs, and the write-path integrity note
The seam README states the JSON-shaped write boundary, watch-disposer
quiescence, async listener containment, and the drained teardown; the
provider README rewrites Behavior around the operation chain,
read-modify-write, writer lock, ready reconcile, and leaf-level YAML
diffs, and updates Known Limitations to the residual guarantees.
A new Agent Note records the round's decisions and supersedes the
original note's deferred-lockfile alternative (cross-linked in place).
Chinese counterparts updated pair-by-pair (three briefed minimal
updates, one whole-document translation); type-equiv, config, cordis,
and module-graph catalogs re-recorded.
2026-07-30 14:09:04 +08:00
|
|
|
|
* rejection is contained and logged like a sync throw. After the disposer
|
|
|
|
|
|
* returns, no further invocation starts — one already queued is skipped;
|
|
|
|
|
|
* one already started still settles, and service disposal waits for it.
|
fix(settings): harden seam and provider per review findings
Confirmed and fixed, each with a regression test that failed first:
- Concurrent update() lost patches (merge over one stale snapshot):
per-namespace serialized write queues; a failed write cannot poison
the queue for later writers.
- Fixed-name .tmp write followed planted symlinks and kept stale modes:
random-suffix sibling, exclusive-create (wx), 0600, cleanup on
failure, then rename.
- A throwing settings/updated listener escaped commit and permanently
wedged the provider reload chain (rejected refreshTask): commit now
contains listener failures (INVARIANT-coded errors still propagate),
async watcher rejections are adopted and contained
(watch callbacks are officially void | Promise<void>), and the
provider chains refreshes on a settled tail with an error log.
- No way to remove a user override: scope/service replace(section)
sets the user section wholesale; replace({}) re-inherits base and
schema defaults.
- The three-primitive provider contract did not hold (base never
called load()): the base Service.init loads and publishes once;
settings-local delegates via yield* super[Service.init]().
- Dispose did not quiesce: teardown flags closed, closes the watcher,
then awaits queued/in-flight reloads; closed is re-checked across
await points.
- Invariant now checks the authoritative relation with the seam's own
deepEqualJson: emitted next must equal settings.get(ns), and
next/prev must differ structurally (cosmokit dependency dropped).
- New docs/core-data-structures/settings.{md,zh.md} with type-equiv
blocks + manifest entries; catalog types moved from exemptions to
LINK_MAP; website page registered.
Both packages stay at per-file 100% coverage.
2026-07-28 18:18:34 +08:00
|
|
|
|
* @param callback - invoked after each commit with the next and previous values.
|
|
|
|
|
|
* @returns the disposer removing this observer.
|
|
|
|
|
|
*/
|
|
|
|
|
|
watch(callback: (next: T, prev: T) => void | Promise<void>): () => void
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Merge a partial patch into this namespace's user layer and persist it.
|
docs(settings): third-review contracts across READMEs, catalogs, and the write-path integrity note
The seam README states the JSON-shaped write boundary, watch-disposer
quiescence, async listener containment, and the drained teardown; the
provider README rewrites Behavior around the operation chain,
read-modify-write, writer lock, ready reconcile, and leaf-level YAML
diffs, and updates Known Limitations to the residual guarantees.
A new Agent Note records the round's decisions and supersedes the
original note's deferred-lockfile alternative (cross-linked in place).
Chinese counterparts updated pair-by-pair (three briefed minimal
updates, one whole-document translation); type-equiv, config, cordis,
and module-graph catalogs re-recorded.
2026-07-30 14:09:04 +08:00
|
|
|
|
* @param patch - plain-object patch over the user section; JSON-shaped data
|
|
|
|
|
|
* only (non-JSON values reject with their path before anything persists).
|
fix(settings): harden seam and provider per review findings
Confirmed and fixed, each with a regression test that failed first:
- Concurrent update() lost patches (merge over one stale snapshot):
per-namespace serialized write queues; a failed write cannot poison
the queue for later writers.
- Fixed-name .tmp write followed planted symlinks and kept stale modes:
random-suffix sibling, exclusive-create (wx), 0600, cleanup on
failure, then rename.
- A throwing settings/updated listener escaped commit and permanently
wedged the provider reload chain (rejected refreshTask): commit now
contains listener failures (INVARIANT-coded errors still propagate),
async watcher rejections are adopted and contained
(watch callbacks are officially void | Promise<void>), and the
provider chains refreshes on a settled tail with an error log.
- No way to remove a user override: scope/service replace(section)
sets the user section wholesale; replace({}) re-inherits base and
schema defaults.
- The three-primitive provider contract did not hold (base never
called load()): the base Service.init loads and publishes once;
settings-local delegates via yield* super[Service.init]().
- Dispose did not quiesce: teardown flags closed, closes the watcher,
then awaits queued/in-flight reloads; closed is re-checked across
await points.
- Invariant now checks the authoritative relation with the seam's own
deepEqualJson: emitted next must equal settings.get(ns), and
next/prev must differ structurally (cosmokit dependency dropped).
- New docs/core-data-structures/settings.{md,zh.md} with type-equiv
blocks + manifest entries; catalog types moved from exemptions to
LINK_MAP; website page registered.
Both packages stay at per-file 100% coverage.
2026-07-28 18:18:34 +08:00
|
|
|
|
*/
|
|
|
|
|
|
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).
|
docs(settings): third-review contracts across READMEs, catalogs, and the write-path integrity note
The seam README states the JSON-shaped write boundary, watch-disposer
quiescence, async listener containment, and the drained teardown; the
provider README rewrites Behavior around the operation chain,
read-modify-write, writer lock, ready reconcile, and leaf-level YAML
diffs, and updates Known Limitations to the residual guarantees.
A new Agent Note records the round's decisions and supersedes the
original note's deferred-lockfile alternative (cross-linked in place).
Chinese counterparts updated pair-by-pair (three briefed minimal
updates, one whole-document translation); type-equiv, config, cordis,
and module-graph catalogs re-recorded.
2026-07-30 14:09:04 +08:00
|
|
|
|
* @param section - the complete next user section; JSON-shaped data only,
|
|
|
|
|
|
* as for {@link update}.
|
fix(settings): harden seam and provider per review findings
Confirmed and fixed, each with a regression test that failed first:
- Concurrent update() lost patches (merge over one stale snapshot):
per-namespace serialized write queues; a failed write cannot poison
the queue for later writers.
- Fixed-name .tmp write followed planted symlinks and kept stale modes:
random-suffix sibling, exclusive-create (wx), 0600, cleanup on
failure, then rename.
- A throwing settings/updated listener escaped commit and permanently
wedged the provider reload chain (rejected refreshTask): commit now
contains listener failures (INVARIANT-coded errors still propagate),
async watcher rejections are adopted and contained
(watch callbacks are officially void | Promise<void>), and the
provider chains refreshes on a settled tail with an error log.
- No way to remove a user override: scope/service replace(section)
sets the user section wholesale; replace({}) re-inherits base and
schema defaults.
- The three-primitive provider contract did not hold (base never
called load()): the base Service.init loads and publishes once;
settings-local delegates via yield* super[Service.init]().
- Dispose did not quiesce: teardown flags closed, closes the watcher,
then awaits queued/in-flight reloads; closed is re-checked across
await points.
- Invariant now checks the authoritative relation with the seam's own
deepEqualJson: emitted next must equal settings.get(ns), and
next/prev must differ structurally (cosmokit dependency dropped).
- New docs/core-data-structures/settings.{md,zh.md} with type-equiv
blocks + manifest entries; catalog types moved from exemptions to
LINK_MAP; website page registered.
Both packages stay at per-file 100% coverage.
2026-07-28 18:18:34 +08:00
|
|
|
|
*/
|
|
|
|
|
|
replace(section: object): Promise<void>
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 描述符
|
|
|
|
|
|
|
2026-07-30 10:53:39 +08:00
|
|
|
|
`describe()` 为配置界面序列化每个已注册 namespace:schemastery 的 `toJSON()` 信封驱动 schema 渲染的表单,解析值填充表单,分离出的 `base`/`user` 层让表单按字段是否出现在 user 层标注「用户已覆盖」。`describe({ redactSecrets: true })`——每个 wire 面都必须传入——从三层剥离 `role('secret')` 字段并枚举其 `{path, set}` 槽位,页面因此能渲染只写输入框而永远收不到机密值。
|
fix(settings): harden seam and provider per review findings
Confirmed and fixed, each with a regression test that failed first:
- Concurrent update() lost patches (merge over one stale snapshot):
per-namespace serialized write queues; a failed write cannot poison
the queue for later writers.
- Fixed-name .tmp write followed planted symlinks and kept stale modes:
random-suffix sibling, exclusive-create (wx), 0600, cleanup on
failure, then rename.
- A throwing settings/updated listener escaped commit and permanently
wedged the provider reload chain (rejected refreshTask): commit now
contains listener failures (INVARIANT-coded errors still propagate),
async watcher rejections are adopted and contained
(watch callbacks are officially void | Promise<void>), and the
provider chains refreshes on a settled tail with an error log.
- No way to remove a user override: scope/service replace(section)
sets the user section wholesale; replace({}) re-inherits base and
schema defaults.
- The three-primitive provider contract did not hold (base never
called load()): the base Service.init loads and publishes once;
settings-local delegates via yield* super[Service.init]().
- Dispose did not quiesce: teardown flags closed, closes the watcher,
then awaits queued/in-flight reloads; closed is re-checked across
await points.
- Invariant now checks the authoritative relation with the seam's own
deepEqualJson: emitted next must equal settings.get(ns), and
next/prev must differ structurally (cosmokit dependency dropped).
- New docs/core-data-structures/settings.{md,zh.md} with type-equiv
blocks + manifest entries; catalog types moved from exemptions to
LINK_MAP; website page registered.
Both packages stay at per-file 100% coverage.
2026-07-28 18:18:34 +08:00
|
|
|
|
|
|
|
|
|
|
```ts type-equiv
|
|
|
|
|
|
/** One registered namespace as surfaced to configuration UIs. */
|
|
|
|
|
|
interface SettingsDescriptor {
|
|
|
|
|
|
/** The registered namespace. */
|
|
|
|
|
|
ns: SettingsNamespace
|
|
|
|
|
|
/** Serialized schemastery schema (`schema.toJSON()`). */
|
|
|
|
|
|
schema: unknown
|
|
|
|
|
|
/** Current resolved value. */
|
|
|
|
|
|
value: unknown
|
feat(settings): detect stale writers with a revision, and announce raw changes
The remaining P1 from the #939 review, plus the P2 it shares a mechanism with.
Nothing carried a version, so two tabs editing one namespace silently
overwrote each other — reproduced as tab B's `reasoning` lost to tab A's
older draft. The seam's per-namespace write queue orders writes; it cannot
tell a fresh writer from one replaying a snapshot a predecessor superseded.
Each namespace now carries a monotonic `revision` over its RAW section. A
write may send `expectedRevision`, checked at the FRONT of the queue (not at
call time, which would race the very predecessor it guards against); a
mismatch rejects with `SettingsConflictError` → `settings-conflict` on the
wire, carrying both revisions. The editor captures the revision it opened at
and, on conflict, asks the user to reopen rather than replaying its snapshot.
The same counter fixes the missing broadcast. `settings/updated` is gated on
the resolved value — correct for consumers, wrong for configuration surfaces:
storing an override equal to the composition base leaves the resolved value
alone while changing what the document says (the field is now overridden, not
inherited) and moving every open editor's revision. `settings/document-updated
(ns, revision)` fires on any raw-section change, in-process or external, and
`host/settings-changed` now rides it.
That event also closes the stale model picker: editing a provider's `models`
changes no route, so `llm/adapters-updated` never fired and an open picker
kept serving the old catalog. A change to an exposed provider namespace now
emits `host/models-changed` too — that namespace holds the catalog.
Docs: both sides of the five touched README pairs, a type-equiv block for
`SettingsPathOp`, and an Agent Note recording what the plane exposes and who
may overwrite what. The deferred wire-redaction gaps (secrets behind
union/intersection/transform, `.default(...)` in the served envelope, schema
text in rejection messages, `new Function` rehydration, pi-ai's `headers`) are
recorded as TODO(settings-wire-redaction) and in Known Limitations rather than
half-fixed.
2026-07-30 19:24:21 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Monotonic revision of the raw user section this descriptor was read at.
|
|
|
|
|
|
* Send it back as `expectedRevision` on a write to refuse a stale one.
|
|
|
|
|
|
*/
|
|
|
|
|
|
revision: number
|
2026-07-30 10:53:39 +08:00
|
|
|
|
/** Registrant's composition `base` layer (detached), when one was declared. */
|
|
|
|
|
|
base?: unknown
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Raw user section from the stored document (detached), when one exists and
|
|
|
|
|
|
* is well-formed; a field's presence here is what marks it user-overridden.
|
|
|
|
|
|
*/
|
|
|
|
|
|
user?: unknown
|
fix(settings): harden seam and provider per review findings
Confirmed and fixed, each with a regression test that failed first:
- Concurrent update() lost patches (merge over one stale snapshot):
per-namespace serialized write queues; a failed write cannot poison
the queue for later writers.
- Fixed-name .tmp write followed planted symlinks and kept stale modes:
random-suffix sibling, exclusive-create (wx), 0600, cleanup on
failure, then rename.
- A throwing settings/updated listener escaped commit and permanently
wedged the provider reload chain (rejected refreshTask): commit now
contains listener failures (INVARIANT-coded errors still propagate),
async watcher rejections are adopted and contained
(watch callbacks are officially void | Promise<void>), and the
provider chains refreshes on a settled tail with an error log.
- No way to remove a user override: scope/service replace(section)
sets the user section wholesale; replace({}) re-inherits base and
schema defaults.
- The three-primitive provider contract did not hold (base never
called load()): the base Service.init loads and publishes once;
settings-local delegates via yield* super[Service.init]().
- Dispose did not quiesce: teardown flags closed, closes the watcher,
then awaits queued/in-flight reloads; closed is re-checked across
await points.
- Invariant now checks the authoritative relation with the seam's own
deepEqualJson: emitted next must equal settings.get(ns), and
next/prev must differ structurally (cosmokit dependency dropped).
- New docs/core-data-structures/settings.{md,zh.md} with type-equiv
blocks + manifest entries; catalog types moved from exemptions to
LINK_MAP; website page registered.
Both packages stay at per-file 100% coverage.
2026-07-28 18:18:34 +08:00
|
|
|
|
/** Owner's declared effect timing. */
|
|
|
|
|
|
applies: SettingsApplies
|
2026-07-30 10:53:39 +08:00
|
|
|
|
/** Schema-declared secret positions; present only under `redactSecrets`. */
|
|
|
|
|
|
secrets?: RedactedSecret[]
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
feat(settings): detect stale writers with a revision, and announce raw changes
The remaining P1 from the #939 review, plus the P2 it shares a mechanism with.
Nothing carried a version, so two tabs editing one namespace silently
overwrote each other — reproduced as tab B's `reasoning` lost to tab A's
older draft. The seam's per-namespace write queue orders writes; it cannot
tell a fresh writer from one replaying a snapshot a predecessor superseded.
Each namespace now carries a monotonic `revision` over its RAW section. A
write may send `expectedRevision`, checked at the FRONT of the queue (not at
call time, which would race the very predecessor it guards against); a
mismatch rejects with `SettingsConflictError` → `settings-conflict` on the
wire, carrying both revisions. The editor captures the revision it opened at
and, on conflict, asks the user to reopen rather than replaying its snapshot.
The same counter fixes the missing broadcast. `settings/updated` is gated on
the resolved value — correct for consumers, wrong for configuration surfaces:
storing an override equal to the composition base leaves the resolved value
alone while changing what the document says (the field is now overridden, not
inherited) and moving every open editor's revision. `settings/document-updated
(ns, revision)` fires on any raw-section change, in-process or external, and
`host/settings-changed` now rides it.
That event also closes the stale model picker: editing a provider's `models`
changes no route, so `llm/adapters-updated` never fired and an open picker
kept serving the old catalog. A change to an exposed provider namespace now
emits `host/models-changed` too — that namespace holds the catalog.
Docs: both sides of the five touched README pairs, a type-equiv block for
`SettingsPathOp`, and an Agent Note recording what the plane exposes and who
may overwrite what. The deferred wire-redaction gaps (secrets behind
union/intersection/transform, `.default(...)` in the served envelope, schema
text in rejection messages, `new Function` rehydration, pi-ai's `headers`) are
recorded as TODO(settings-wire-redaction) and in Known Limitations rather than
half-fixed.
2026-07-30 19:24:21 +08:00
|
|
|
|
只持有脱敏 descriptor 的调用方无法安全地重建分节,因此删除改以路径 op 传递。每个 descriptor 还携带针对原始分节的 `revision`;写入可以把它作为 `expectedRevision` 送回,不再匹配的写入会被拒绝,而不是覆盖在先落地的那个写方之上。
|
|
|
|
|
|
```ts type-equiv
|
|
|
|
|
|
/**
|
|
|
|
|
|
* One path-addressed edit to a namespace's user section. Path mutation exists
|
|
|
|
|
|
* for a caller holding an INCOMPLETE view of the section — a configuration UI
|
|
|
|
|
|
* reads the redacted descriptor, which by construction never received the
|
|
|
|
|
|
* `role('secret')` fields. Such a caller can name the field it means without
|
|
|
|
|
|
* restating the section: a wholesale `replace` rebuilt from a redacted
|
|
|
|
|
|
* document silently deletes every secret the wire never returned.
|
|
|
|
|
|
*/
|
|
|
|
|
|
type SettingsPathOp =
|
|
|
|
|
|
| { op: 'set'; path: readonly string[]; value: unknown }
|
|
|
|
|
|
| { op: 'unset'; path: readonly string[] }
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-30 10:53:39 +08:00
|
|
|
|
```ts type-equiv
|
|
|
|
|
|
/** Options for {@link Settings.describe}. */
|
|
|
|
|
|
interface SettingsDescribeOptions {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Strip `role('secret')` fields from `value`/`base`/`user` and enumerate
|
|
|
|
|
|
* them in each descriptor's `secrets`. Every wire surface MUST pass this;
|
|
|
|
|
|
* the verbatim default exists for same-process configuration UIs only.
|
|
|
|
|
|
*/
|
|
|
|
|
|
redactSecrets?: boolean
|
fix(settings): harden seam and provider per review findings
Confirmed and fixed, each with a regression test that failed first:
- Concurrent update() lost patches (merge over one stale snapshot):
per-namespace serialized write queues; a failed write cannot poison
the queue for later writers.
- Fixed-name .tmp write followed planted symlinks and kept stale modes:
random-suffix sibling, exclusive-create (wx), 0600, cleanup on
failure, then rename.
- A throwing settings/updated listener escaped commit and permanently
wedged the provider reload chain (rejected refreshTask): commit now
contains listener failures (INVARIANT-coded errors still propagate),
async watcher rejections are adopted and contained
(watch callbacks are officially void | Promise<void>), and the
provider chains refreshes on a settled tail with an error log.
- No way to remove a user override: scope/service replace(section)
sets the user section wholesale; replace({}) re-inherits base and
schema defaults.
- The three-primitive provider contract did not hold (base never
called load()): the base Service.init loads and publishes once;
settings-local delegates via yield* super[Service.init]().
- Dispose did not quiesce: teardown flags closed, closes the watcher,
then awaits queued/in-flight reloads; closed is re-checked across
await points.
- Invariant now checks the authoritative relation with the seam's own
deepEqualJson: emitted next must equal settings.get(ns), and
next/prev must differ structurally (cosmokit dependency dropped).
- New docs/core-data-structures/settings.{md,zh.md} with type-equiv
blocks + manifest entries; catalog types moved from exemptions to
LINK_MAP; website page registered.
Both packages stay at per-file 100% coverage.
2026-07-28 18:18:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 变更提交
|
|
|
|
|
|
|
|
|
|
|
|
每次提交的变更——进程内写入或 provider 观察到的外部编辑——在新值成为权威值之后发出 `settings/updated (ns, next, prev, source)`,解析值深相等时绝不发出。source 标记区分两条入口路径。
|
|
|
|
|
|
|
|
|
|
|
|
```ts type-equiv
|
|
|
|
|
|
/** Origin of one committed settings change. */
|
|
|
|
|
|
type SettingsUpdateSource = 'update' | 'provider'
|
|
|
|
|
|
```
|