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:
@@ -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
|
||||
|
||||
@@ -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' })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user