refactor(core): isolate compatibility skills

This commit is contained in:
Dax Raad
2026-09-13 19:11:13 -04:00
parent 199aabe9e2
commit 7784b3ee0d
13 changed files with 150 additions and 179 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ Review endpoints in document order. For each endpoint, select one disposition an
| [x] 018 | `GET` | `/api/command` | `command.list` | Keep | Request and response accepted as-is. |
| [x] 019 | `GET` | `/api/skill` | `skill.list` | Keep | Renamed `location` to `path`; removed the skill-specific `slash` flag and slash-command behavior. |
| [x] 020 | `GET` | `/api/reference` | `reference.list` | Keep | Removed duplicate `description` and `hidden` fields from nested `source`. |
| [ ] 021 | `GET` | `/api/config` | `config.get` | | |
| [ ] 021 | `GET` | `/api/config` | `config.get` | Change | Removed `claude` and `agents` entry variants; one compatibility plugin now normalizes their skills. |
| [ ] 022 | `GET` | `/api/config/preferences` | `config.preferences` | | |
| [ ] 023 | `PATCH` | `/api/config/preferences` | `config.updatePreferences` | | |
| [ ] 024 | `GET` | `/api/config/shell` | `config.shells` | | |
@@ -2092,8 +2092,6 @@ export type ConfigEntry =
}
}
| { type: "directory"; path: string }
| { type: "agents"; path: string }
| { type: "claude"; path: string }
export type SessionInboxUser = {
id: string
+28 -20
View File
@@ -5,17 +5,7 @@ import path from "path"
import { isDeepStrictEqual } from "node:util"
import { applyEdits, modify, type ParseError, parse } from "jsonc-parser"
import { Context, Effect, FiberMap, Layer, Option, PubSub, Ref, Schema, Semaphore, Stream } from "effect"
import {
AgentsDirectory,
ClaudeDirectory,
Directory,
Document,
Info,
type Preferences,
type PreferencesPatch,
type Entry,
Event,
} from "@opencode/schema/config"
import { Directory, Document, Info, type Preferences, type PreferencesPatch, type Entry, Event } from "@opencode/schema/config"
import { Credential } from "./credential.js"
import { Bus } from "./bus.js"
import { Watcher } from "./filesystem/watcher.js"
@@ -37,6 +27,11 @@ export function latest<K extends keyof Info>(entries: readonly Entry[], key: K):
export interface Interface {
/** Returns location config documents and discovery sources from lowest to highest priority. */
readonly entries: () => Effect.Effect<Entry[]>
/** Compatibility roots consumed by internal compatibility plugins. */
readonly compatibility?: () => Effect.Effect<{
readonly claude: readonly AbsolutePath[]
readonly agents: readonly AbsolutePath[]
}>
/**
* Streams raw filesystem updates under config roots. Config owns root
* topology and watch reconciliation; domain owners filter this feed for the
@@ -71,13 +66,20 @@ export interface TestInterface extends Interface {
export class Test extends Context.Service<Test, TestInterface>()("@opencode/Config/Test") {}
/** In-memory config for tests: static entries with replaceable state and a test-driven change feed. */
export const testLayer = (initial: Entry[] = []) =>
export const testLayer = (
initial: Entry[] = [],
compatibility: { readonly claude: readonly AbsolutePath[]; readonly agents: readonly AbsolutePath[] } = {
claude: [],
agents: [],
},
) =>
Layer.effectContext(
Effect.gen(function* () {
const entries = yield* Ref.make(initial)
const updates = yield* PubSub.unbounded<Watcher.Update>()
const service = Test.of({
entries: () => Ref.get(entries),
compatibility: () => Effect.succeed(compatibility),
changes: () => Stream.fromPubSub(updates),
setEntries: (next) => Ref.set(entries, next),
emitChange: (update) => PubSub.publish(updates, update).pipe(Effect.asVoid),
@@ -205,8 +207,6 @@ export const layer = (options?: Options) =>
})
const load = Effect.fn("Config.load")(function* (sources: ConfigDiscovery.Sources) {
const claude = yield* Effect.filter(sources.claude, (path) => fs.isDir(path))
const agents = yield* Effect.filter(sources.agents, (path) => fs.isDir(path))
const direct = yield* Effect.forEach(sources.direct, (filepath) => loadFile(filepath)).pipe(
Effect.orDie,
Effect.map((entries) => entries.filter((entry): entry is Document => entry !== undefined)),
@@ -244,8 +244,6 @@ export const layer = (options?: Options) =>
)
return [
...(yield* loadWellknown().pipe(Effect.orDie)),
...claude.map((path) => new ClaudeDirectory({ type: "claude", path })),
...agents.map((path) => new AgentsDirectory({ type: "agents", path })),
...globalSupplementary,
...explicit,
...direct,
@@ -255,6 +253,7 @@ export const layer = (options?: Options) =>
})
const initial = yield* ConfigDiscovery.discover(options)
let sources = initial
let configs = yield* load(initial)
const updates = yield* PubSub.unbounded<Watcher.Update>()
const reloads = yield* PubSub.sliding<void>(1)
@@ -280,10 +279,14 @@ export const layer = (options?: Options) =>
const reload = Effect.fn("Config.reload")(
function* () {
const sources = yield* ConfigDiscovery.discover(options)
const next = yield* load(sources)
yield* reconcile(sources)
if (isDeepStrictEqual(configs, next)) return
const discovered = yield* ConfigDiscovery.discover(options)
const next = yield* load(discovered)
yield* reconcile(discovered)
const compatibilityChanged =
!isDeepStrictEqual(sources.claude, discovered.claude) ||
!isDeepStrictEqual(sources.agents, discovered.agents)
if (isDeepStrictEqual(configs, next) && !compatibilityChanged) return
sources = discovered
configs = next
yield* bus.publish(Event.Updated, {})
},
@@ -379,6 +382,11 @@ export const layer = (options?: Options) =>
entries: Effect.fnUntraced(function* () {
return configs
}),
compatibility: () =>
Effect.all({
claude: Effect.filter(sources.claude, fs.isDir),
agents: Effect.filter(sources.agents, fs.isDir),
}),
changes: () => Stream.fromPubSub(updates),
preferences,
updatePreferences,
@@ -0,0 +1,78 @@
export * as ConfigCompatibilityPlugin from "./compatibility.js"
import { define } from "@opencode/plugin/effect/plugin"
import { FSUtil } from "@opencode/util/fs-util"
import path from "path"
import { Effect, FiberMap, PubSub, Semaphore, Stream } from "effect"
import { Config } from "../../config.js"
import { Watcher } from "../../filesystem/watcher.js"
import { AbsolutePath } from "../../schema.js"
import { Skill } from "../../skill.js"
import { SkillFile } from "./skill-file.js"
export const Plugin = define({
id: "opencode.config.compatibility",
effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service
const fs = yield* FSUtil.Service
const watcher = yield* Watcher.Service
const watches = yield* FiberMap.make<string>()
const changes = yield* PubSub.sliding<string>(1)
const lock = Semaphore.makeUnsafe(1)
const skills: Skill.Info[] = []
const watch = Effect.fn("ConfigCompatibilityPlugin.watch")(function* (
target: string,
type: "file" | "directory",
) {
const updates = yield* watcher.subscribe({ path: target, type })
yield* FiberMap.run(
watches,
`${type}:${target}`,
updates.pipe(Stream.runForEach((update) => PubSub.publish(changes, update.path).pipe(Effect.asVoid))),
{ onlyIfMissing: true, startImmediately: true },
)
})
const refresh = Effect.fn("ConfigCompatibilityPlugin.refresh")(
function* () {
yield* FiberMap.clear(watches)
const roots = config.compatibility ? yield* config.compatibility() : { claude: [], agents: [] }
const directories = [...roots.claude, ...roots.agents].map((root) => path.join(root, "skills"))
const loaded = new Map<Skill.ID, Skill.Info>()
for (const directory of directories) {
const resolved = yield* fs.realPath(directory).pipe(Effect.orElseSucceed(() => undefined))
if (!resolved) continue
yield* watch(resolved, "directory")
const files = yield* fs
.scan("{*.md,**/SKILL.md}", { cwd: resolved, absolute: true, include: "file", symlink: true, dot: true })
.pipe(Effect.orElseSucceed(() => [] as string[]))
for (const filepath of files.toSorted()) {
const content = yield* fs.readFileStringSafe(filepath).pipe(Effect.orElseSucceed(() => undefined))
if (!content) continue
const parsed = SkillFile.parse(resolved, filepath, content)
if (parsed._tag === "Parsed") loaded.set(parsed.skill.id, parsed.skill)
}
}
skills.splice(0, skills.length, ...loaded.values())
},
(effect) => lock.withPermit(effect),
)
const reload = refresh().pipe(Effect.andThen(ctx.skill.reload()))
const updates = yield* PubSub.subscribe(changes)
yield* Stream.fromSubscription(updates).pipe(
Stream.debounce("100 millis"),
Stream.runForEach(() => reload),
Effect.forkScoped({ startImmediately: true }),
)
yield* config.changes().pipe(
Stream.runForEach(() => reload),
Effect.forkScoped({ startImmediately: true }),
)
yield* refresh()
yield* ctx.skill.transform((editor) => {
for (const skill of skills) editor.add(skill)
})
}),
})
-5
View File
@@ -80,13 +80,8 @@ export const Plugin = define({
if (result.some((item) => Skill.Source.equals(item, source))) return
result.push(source)
}
const claude = loaded.entries.flatMap((entry) => (entry.type === "claude" ? [entry.path] : []))
const agents = loaded.entries.flatMap((entry) => (entry.type === "agents" ? [entry.path] : []))
const directories = loaded.entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
const items = loaded.entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
for (const directory of [...claude, ...agents]) {
add(Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }))
}
for (const directory of directories) {
add(Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }))
add(Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }))
+2
View File
@@ -25,6 +25,7 @@ import { ConfigReferencePlugin } from "../config/plugin/reference.js"
import { ConfigShellPlugin } from "../config/plugin/shell.js"
import { ConfigSnapshotPlugin } from "../config/plugin/snapshot.js"
import { ConfigSkillPlugin } from "../config/plugin/skill.js"
import { ConfigCompatibilityPlugin } from "../config/plugin/compatibility.js"
import { ConfigToolOutputPlugin } from "../config/plugin/tool-output.js"
import { ConfigWebSearchPlugin } from "../config/plugin/websearch.js"
import { ConfigWorktreePlugin } from "../config/plugin/worktree.js"
@@ -236,6 +237,7 @@ const post = [
ConfigShellPlugin.Plugin,
ConfigSnapshotPlugin.Plugin,
ConfigToolOutputPlugin.Plugin,
ConfigCompatibilityPlugin.Plugin,
ConfigSkillPlugin.Plugin,
ConfigProviderPlugin.Plugin,
ConfigWebSearchPlugin.Plugin,
+10 -21
View File
@@ -4,7 +4,7 @@ import { describe, expect, test } from "bun:test"
import { Effect, Fiber, Layer, Logger, Schema, Stream } from "effect"
import { FastCheck } from "effect/testing"
import { Config } from "@opencode/core/config"
import { AgentsDirectory, Directory, Document, Event, Info } from "@opencode/schema/config"
import { Directory, Document, Event, Info } from "@opencode/schema/config"
import { ConfigModel } from "@opencode/schema/config/model"
import { ConfigProvider } from "@opencode/schema/config/provider"
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
@@ -83,8 +83,6 @@ describe("Config", () => {
const global = path.join(tmp.path, "global")
const home = path.join(global, "home")
const project = path.join(home, "project")
const ambient = (entries: readonly { type: string }[]) =>
entries.filter((entry) => entry.type === "claude" || entry.type === "agents")
return Effect.promise(() =>
Promise.all([
fs.mkdir(project, { recursive: true }),
@@ -96,7 +94,8 @@ describe("Config", () => {
Effect.gen(function* () {
// The fixture is real: with global enabled the walk finds both.
const config = yield* Config.Service
expect(ambient(yield* config.entries()).length).toBe(2)
const compatibility = yield* config.compatibility!()
expect([...compatibility.claude, ...compatibility.agents]).toHaveLength(2)
}).pipe(Effect.provide(testLayer(project, global))),
),
Effect.andThen(
@@ -105,7 +104,7 @@ describe("Config", () => {
// project walk enabled.
Effect.gen(function* () {
const config = yield* Config.Service
expect(ambient(yield* config.entries())).toEqual([])
expect(yield* config.compatibility!()).toEqual({ claude: [], agents: [] })
const watcher = yield* Watcher.Test
expect(
(yield* watcher.subscriptions()).filter((watch) => watch.type === "entries" && watch.path === home),
@@ -431,7 +430,6 @@ describe("Config", () => {
info: new Info({ model: selection("openrouter/openai/gpt-5") }),
}),
new Directory({ type: "directory", path: AbsolutePath.make("/skills") }),
new AgentsDirectory({ type: "agents", path: AbsolutePath.make("/agents") }),
new Document({ type: "document", info: new Info({}) }),
new Document({
type: "document",
@@ -912,16 +910,12 @@ describe("Config", () => {
fs.mkdir(project, { recursive: true }),
]),
)
const entries = yield* Config.Service.use((config) => config.entries()).pipe(
const compatibility = yield* Config.Service.use((config) => config.compatibility!()).pipe(
Effect.provide(testLayer(project, global)),
)
expect(entries.filter((entry) => entry.type === "claude").map((entry) => entry.path)).toEqual([
AbsolutePath.make(path.join(home, ".claude")),
])
expect(entries.filter((entry) => entry.type === "agents").map((entry) => entry.path)).toEqual([
AbsolutePath.make(path.join(home, ".agents")),
])
expect(compatibility.claude).toEqual([AbsolutePath.make(path.join(home, ".claude"))])
expect(compatibility.agents).toEqual([AbsolutePath.make(path.join(home, ".agents"))])
}),
),
),
@@ -1560,18 +1554,19 @@ describe("Config", () => {
const config = yield* Config.Service
const entries = (yield* config.entries()).filter((entry) => !entry.path || inFixture(tmp.path, entry.path))
const documents = entries.filter((entry) => entry.type === "document")
const compatibility = yield* config.compatibility!()
expect(entries.filter((entry) => entry.type === "directory").map((entry) => entry.path)).toEqual([
AbsolutePath.make(global),
AbsolutePath.make(path.join(root, ".opencode")),
AbsolutePath.make(path.join(directory, ".opencode")),
])
expect(entries.filter((entry) => entry.type === "agents").map((entry) => entry.path)).toEqual([
expect(compatibility.agents.filter((path) => inFixture(tmp.path, path))).toEqual([
AbsolutePath.make(globalAgents),
AbsolutePath.make(path.join(root, ".agents")),
AbsolutePath.make(path.join(directory, ".agents")),
])
expect(entries.filter((entry) => entry.type === "claude").map((entry) => entry.path)).toEqual([
expect(compatibility.claude.filter((path) => inFixture(tmp.path, path))).toEqual([
AbsolutePath.make(globalClaude),
AbsolutePath.make(path.join(root, ".claude")),
AbsolutePath.make(path.join(directory, ".claude")),
@@ -1586,12 +1581,6 @@ describe("Config", () => {
"directory-dot",
])
expect(entries.map((entry) => (entry.type === "document" ? entry.info.$schema : entry.path))).toEqual([
AbsolutePath.make(globalClaude),
AbsolutePath.make(path.join(root, ".claude")),
AbsolutePath.make(path.join(directory, ".claude")),
AbsolutePath.make(globalAgents),
AbsolutePath.make(path.join(root, ".agents")),
AbsolutePath.make(path.join(directory, ".agents")),
"global",
AbsolutePath.make(global),
"outside",
+29 -14
View File
@@ -3,8 +3,9 @@ import path from "path"
import { describe, expect, test } from "bun:test"
import { Deferred, Effect, Fiber, Layer, Schema, Stream } from "effect"
import { Config } from "@opencode/core/config"
import { AgentsDirectory, ClaudeDirectory, Directory, Document, type Entry, Info } from "@opencode/schema/config"
import { Directory, Document, type Entry, Info } from "@opencode/schema/config"
import { ConfigSkillPlugin } from "@opencode/core/config/plugin/skill"
import { ConfigCompatibilityPlugin } from "@opencode/core/config/plugin/compatibility"
import { SkillFile } from "@opencode/core/config/plugin/skill-file"
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
import { Watcher } from "@opencode/core/filesystem/watcher"
@@ -47,18 +48,26 @@ const startEntries = Effect.fnUntraced(function* (
directory: string,
home = directory,
discovery = emptyDiscovery,
compatibility: { readonly claude: readonly AbsolutePath[]; readonly agents: readonly AbsolutePath[] } = {
claude: [],
agents: [],
},
) {
const service = yield* Skill.Service
const pluginHost = host({
skill: {
list: () => Effect.die("unused skill.list"),
transform: service.transform,
reload: service.reload,
},
})
yield* ConfigCompatibilityPlugin.Plugin.effect(pluginHost).pipe(
Effect.provide(Config.testLayer(entries, compatibility)),
)
yield* ConfigSkillPlugin.Plugin.effect(
host({
skill: {
list: () => Effect.die("unused skill.list"),
transform: service.transform,
reload: service.reload,
},
}),
pluginHost,
).pipe(
Effect.provide(Config.testLayer(entries)),
Effect.provide(Config.testLayer(entries, compatibility)),
Effect.provideService(SkillDiscovery.Service, discovery),
Effect.provideService(Global.Service, Global.Service.of({ ...Global.make(), home })),
Effect.provideService(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
@@ -82,7 +91,7 @@ const start = (skills: string[], directory: string, discovery = emptyDiscovery)
const discover = (directory: string, global: string) =>
Effect.gen(function* () {
const config = yield* Config.Service
return yield* config.entries()
return { entries: yield* config.entries(), compatibility: yield* config.compatibility!() }
}).pipe(
Effect.provide(
AppNodeBuilder.build(LayerNode.group([Config.node, Bus.node]), [
@@ -175,13 +184,13 @@ describe("ConfigSkillPlugin.Plugin", () => {
yield* startEntries(
[
new ClaudeDirectory({ type: "claude", path: AbsolutePath.make(claude) }),
new AgentsDirectory({ type: "agents", path: AbsolutePath.make(agents) }),
new Directory({ type: "directory", path: AbsolutePath.make(opencode) }),
new Document({ type: "document", info: decode({ skills: ["~/shared", "./relative"] }) }),
],
directory,
home,
emptyDiscovery,
{ claude: [AbsolutePath.make(claude)], agents: [AbsolutePath.make(agents)] },
)
const watcher = yield* Watcher.Test
expect(yield* watcher.subscriptions()).toEqual(expected.map((item) => ({ path: item, type: "directory" })))
@@ -245,8 +254,14 @@ describe("ConfigSkillPlugin.Plugin", () => {
await write(worktreeSkills, "review", "Worktree")
})
const entries = yield* discover(worktree, path.join(tmp.path, "global"))
const skill = yield* startEntries(entries, worktree)
const discovered = yield* discover(worktree, path.join(tmp.path, "global"))
const skill = yield* startEntries(
discovered.entries,
worktree,
worktree,
emptyDiscovery,
discovered.compatibility,
)
const review = (yield* skill.list()).find((item) => item.id === "review")
expect(review?.description).toBe("Worktree")
-34
View File
@@ -13048,34 +13048,6 @@
},
"additionalProperties": false
},
"Config.AgentsDirectoryEncoded": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["agents"]
},
"path": {
"type": "string"
}
},
"required": ["type", "path"],
"additionalProperties": false
},
"Config.ClaudeDirectoryEncoded": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["claude"]
},
"path": {
"type": "string"
}
},
"required": ["type", "path"],
"additionalProperties": false
},
"Config.CommandEncoded": {
"type": "object",
"properties": {
@@ -13164,12 +13136,6 @@
},
{
"$ref": "#/components/schemas/Config.DirectoryEncoded"
},
{
"$ref": "#/components/schemas/Config.AgentsDirectoryEncoded"
},
{
"$ref": "#/components/schemas/Config.ClaudeDirectoryEncoded"
}
]
},
+1 -11
View File
@@ -132,17 +132,7 @@ export class Directory extends Schema.Class<Directory>("Config.Directory")({
path: AbsolutePath,
}) {}
export class AgentsDirectory extends Schema.Class<AgentsDirectory>("Config.AgentsDirectory")({
type: Schema.Literal("agents"),
path: AbsolutePath,
}) {}
export class ClaudeDirectory extends Schema.Class<ClaudeDirectory>("Config.ClaudeDirectory")({
type: Schema.Literal("claude"),
path: AbsolutePath,
}) {}
export const Entry = Schema.Union([Document, Directory, AgentsDirectory, ClaudeDirectory]).annotate({
export const Entry = Schema.Union([Document, Directory]).annotate({
identifier: "Config.Entry",
})
export type Entry = typeof Entry.Type
+1 -3
View File
@@ -49,8 +49,6 @@ describe("Config.Entry", () => {
}),
new Config.Document({ type: "document", info: new Config.Info({ shell: "/bin/zsh" }) }),
new Config.Directory({ type: "directory", path: AbsolutePath.make("/project/.opencode") }),
new Config.AgentsDirectory({ type: "agents", path: AbsolutePath.make("/project/.agents") }),
new Config.ClaudeDirectory({ type: "claude", path: AbsolutePath.make("/project/.claude") }),
]
const encoded = Schema.encodeSync(Schema.Array(Config.Entry))(entries)
@@ -59,7 +57,7 @@ describe("Config.Entry", () => {
expect(decoded).toEqual(entries)
expect(decoded[0]).toBeInstanceOf(Config.Document)
expect(decoded[1]).not.toHaveProperty("path")
expect(decoded.map((entry) => entry.type)).toEqual(["document", "document", "directory", "agents", "claude"])
expect(decoded.map((entry) => entry.type)).toEqual(["document", "document", "directory"])
expect(decoded[0]?.type === "document" ? decoded[0].info.permissions : undefined).toEqual([
{ action: "shell", resource: "*", effect: "ask" },
{ action: "shell", resource: "git status", effect: "allow" },
-34
View File
@@ -13048,34 +13048,6 @@
},
"additionalProperties": false
},
"Config.AgentsDirectoryEncoded": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["agents"]
},
"path": {
"type": "string"
}
},
"required": ["type", "path"],
"additionalProperties": false
},
"Config.ClaudeDirectoryEncoded": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["claude"]
},
"path": {
"type": "string"
}
},
"required": ["type", "path"],
"additionalProperties": false
},
"Config.CommandEncoded": {
"type": "object",
"properties": {
@@ -13164,12 +13136,6 @@
},
{
"$ref": "#/components/schemas/Config.DirectoryEncoded"
},
{
"$ref": "#/components/schemas/Config.AgentsDirectoryEncoded"
},
{
"$ref": "#/components/schemas/Config.ClaudeDirectoryEncoded"
}
]
},
-34
View File
@@ -13048,34 +13048,6 @@
},
"additionalProperties": false
},
"Config.AgentsDirectoryEncoded": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["agents"]
},
"path": {
"type": "string"
}
},
"required": ["type", "path"],
"additionalProperties": false
},
"Config.ClaudeDirectoryEncoded": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["claude"]
},
"path": {
"type": "string"
}
},
"required": ["type", "path"],
"additionalProperties": false
},
"Config.CommandEncoded": {
"type": "object",
"properties": {
@@ -13164,12 +13136,6 @@
},
{
"$ref": "#/components/schemas/Config.DirectoryEncoded"
},
{
"$ref": "#/components/schemas/Config.AgentsDirectoryEncoded"
},
{
"$ref": "#/components/schemas/Config.ClaudeDirectoryEncoded"
}
]
},