docs: bilingual credentials/settings-consumer documentation, catalogs, and gates

New credentials data-structure page (type-equiv manifested), group README,
rewritten llm-deepseek/llm-pi-ai READMEs (dynamic configuration, dict
profiles, credential chain), capability-seams/service-role registration,
Agent Note (bilingual), demo compositions mounting settings-local +
credentials-local with no inline key plumbing, installSettingsSection
consumer helper on the settings seam (deduplicating both adapters' wiring),
jscpd symmetry markers for the provider twins, runtime-closure additions for
python/sdk-runtime, and doc-budget ceilings AGENTS.md 1750→1755 /
packages/README.md 850→865 for the structural one-line group rows.
This commit is contained in:
Yichen Jiang
2026-07-29 14:20:06 +08:00
parent d77db29f01
commit b0a2011d95
61 changed files with 732 additions and 153 deletions

View File

@@ -442,4 +442,54 @@ export abstract class Settings extends Service {
}
}
/** Hooks a consumer hands to {@link installSettingsSection}. */
export interface SettingsSectionHooks<T> {
/**
* Receive the active configuration source: the resolved settings scope
* while one is attached, the composition entry otherwise. Called before
* the matching `onChange` at attach and at detach.
* @param current - thunk returning the currently authoritative value.
*/
setSource(current: () => T): void
/**
* Re-judge anything derived from the source — registration-level facts,
* memoized resolutions — after an attach, a detach, or a committed change.
*/
onChange(): void
}
/**
* Install the canonical optional-settings consumer wiring: while a settings
* service exists, register `ns` with the consumer's composition entry as the
* `base` layer and point the source thunk at the resolved scope; when the
* service goes away (disposal, provider reload), fall back to the entry so
* the consumer keeps working exactly as composed. The registration rides the
* scoped fiber, so no settings service ever mounted means none of this runs.
* @param ctx - consumer plugin context owning the wiring.
* @param ns - the consumer-owned settings namespace.
* @param schema - schema resolving the namespace (typically the plugin Config).
* @param entry - the consumer's composition entry config, used as `base`.
* @param hooks - source sink and change notification.
*/
export function installSettingsSection<T>(
ctx: Context,
ns: SettingsNamespace,
schema: z<T>,
entry: T,
hooks: SettingsSectionHooks<T>,
): void {
ctx.inject(['settings'], (sctx) => {
const scope = sctx.settings.register(ns, schema, { base: entry })
hooks.setSource(() => scope.get())
sctx.effect(() => () => {
hooks.setSource(() => entry)
hooks.onChange()
})
hooks.onChange()
scope.watch(() => {
hooks.onChange()
})
})
}
export default Settings

View File

@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import z from 'schemastery'
import { Settings, deepEqualJson, settingsNamespace, type SettingsNamespace, type SettingsScope, type SettingsUpdateSource } from '../src/index.ts'
import { Settings, deepEqualJson, installSettingsSection, settingsNamespace, type SettingsNamespace, type SettingsScope, type SettingsUpdateSource } from '../src/index.ts'
import { MemorySettings } from './memory.ts'
/** A provider implementing only the three primitives: the seam owns init. */
@@ -558,3 +558,46 @@ describe('watch', () => {
expect(scope.get()).toEqual({ theme: 'light', fontSize: 14 })
})
})
describe('installSettingsSection', () => {
const HelperSchema: z<{ theme: string }> = z.object({
theme: z.string().default('default'),
})
it('drives the source through attach, live commits, and detach', async () => {
const ctx = new Context()
const entry = { theme: 'entry' }
let current: () => { theme: string } = () => entry
let changes = 0
installSettingsSection(ctx, settingsNamespace('helper-ns'), HelperSchema, entry, {
setSource: (source) => {
current = source
},
onChange: () => {
changes += 1
},
})
// No settings service mounted: nothing ran, the entry stays authoritative.
expect(current()).toEqual({ theme: 'entry' })
expect(changes).toBe(0)
const fiber = ctx.plugin(MemorySettings, { doc: { 'helper-ns': { theme: 'user' } } })
await fiber
await vi.waitFor(() => {
expect(current()).toEqual({ theme: 'user' })
})
expect(changes).toBe(1)
await ctx.settings.update(settingsNamespace('helper-ns'), { theme: 'live' })
await vi.waitFor(() => {
expect(changes).toBe(2)
})
expect(current()).toEqual({ theme: 'live' })
await fiber.dispose()
await vi.waitFor(() => {
expect(changes).toBe(3)
})
expect(current()).toEqual({ theme: 'entry' })
})
})