refactor(core): extract SystemContext.diffByKey for delta renderers
Skill, reference, and MCP guidance shared the same keyed three-way diff mechanics; each keeps only its own changed comparator and phrasing.
This commit is contained in:
parent
2fee823dbf
commit
3898cac63d
5 changed files with 92 additions and 39 deletions
|
|
@ -25,28 +25,26 @@ const render = (servers: ReadonlyArray<Summary>) =>
|
|||
["<mcp_instructions>", ...entries(servers), "</mcp_instructions>"].join("\n")
|
||||
|
||||
const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary>) => {
|
||||
const names = new Set(current.map((server) => server.server))
|
||||
const previousByName = new Map(previous.map((server) => [server.server, server]))
|
||||
const added = current.filter((server) => !previousByName.has(server.server))
|
||||
const removed = previous.filter((server) => !names.has(server.server))
|
||||
const changed = current.filter((server) => {
|
||||
const before = previousByName.get(server.server)
|
||||
return before !== undefined && before.instructions !== server.instructions
|
||||
})
|
||||
const diff = SystemContext.diffByKey(
|
||||
previous,
|
||||
current,
|
||||
(server) => server.server,
|
||||
(before, after) => before.instructions !== after.instructions,
|
||||
)
|
||||
// Additions and removals render as small deltas; anything else restates the full list.
|
||||
if (changed.length > 0 || (added.length === 0 && removed.length === 0))
|
||||
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
||||
return [
|
||||
"The available MCP server instructions have changed. This list supersedes the previous one.",
|
||||
render(current),
|
||||
].join("\n")
|
||||
return [
|
||||
...(added.length === 0
|
||||
...(diff.added.length === 0
|
||||
? []
|
||||
: ["New MCP server instructions are available in addition to those previously listed:", ...entries(added)]),
|
||||
...(removed.length === 0
|
||||
: ["New MCP server instructions are available in addition to those previously listed:", ...entries(diff.added)]),
|
||||
...(diff.removed.length === 0
|
||||
? []
|
||||
: [
|
||||
`Instructions for the following MCP servers are no longer available: ${removed.map((server) => server.server).join(", ")}.`,
|
||||
`Instructions for the following MCP servers are no longer available: ${diff.removed.map((server) => server.server).join(", ")}.`,
|
||||
]),
|
||||
].join("\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,28 +29,26 @@ const render = (references: ReadonlyArray<typeof Summary.Type>) =>
|
|||
].join("\n")
|
||||
|
||||
const update = (previous: ReadonlyArray<typeof Summary.Type>, current: ReadonlyArray<typeof Summary.Type>) => {
|
||||
const names = new Set(current.map((reference) => reference.name))
|
||||
const previousByName = new Map(previous.map((reference) => [reference.name, reference]))
|
||||
const added = current.filter((reference) => !previousByName.has(reference.name))
|
||||
const removed = previous.filter((reference) => !names.has(reference.name))
|
||||
const changed = current.filter((reference) => {
|
||||
const before = previousByName.get(reference.name)
|
||||
return before !== undefined && (before.path !== reference.path || before.description !== reference.description)
|
||||
})
|
||||
const diff = SystemContext.diffByKey(
|
||||
previous,
|
||||
current,
|
||||
(reference) => reference.name,
|
||||
(before, after) => before.path !== after.path || before.description !== after.description,
|
||||
)
|
||||
// Additions and removals render as small deltas; anything else restates the full list.
|
||||
if (changed.length > 0 || (added.length === 0 && removed.length === 0))
|
||||
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
||||
return [
|
||||
"The available project references have changed. This list supersedes the previous reference list.",
|
||||
render(current),
|
||||
].join("\n")
|
||||
return [
|
||||
...(added.length === 0
|
||||
...(diff.added.length === 0
|
||||
? []
|
||||
: ["New project references are available in addition to those previously listed:", ...entries(added)]),
|
||||
...(removed.length === 0
|
||||
: ["New project references are available in addition to those previously listed:", ...entries(diff.added)]),
|
||||
...(diff.removed.length === 0
|
||||
? []
|
||||
: [
|
||||
`The following project references are no longer available and must not be used: ${removed.map((reference) => reference.name).join(", ")}.`,
|
||||
`The following project references are no longer available and must not be used: ${diff.removed.map((reference) => reference.name).join(", ")}.`,
|
||||
]),
|
||||
].join("\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,28 +31,26 @@ const render = (skills: ReadonlyArray<Summary>) =>
|
|||
].join("\n")
|
||||
|
||||
const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary>) => {
|
||||
const names = new Set(current.map((skill) => skill.name))
|
||||
const previousByName = new Map(previous.map((skill) => [skill.name, skill]))
|
||||
const added = current.filter((skill) => !previousByName.has(skill.name))
|
||||
const removed = previous.filter((skill) => !names.has(skill.name))
|
||||
const changed = current.filter((skill) => {
|
||||
const before = previousByName.get(skill.name)
|
||||
return before !== undefined && before.description !== skill.description
|
||||
})
|
||||
const diff = SystemContext.diffByKey(
|
||||
previous,
|
||||
current,
|
||||
(skill) => skill.name,
|
||||
(before, after) => before.description !== after.description,
|
||||
)
|
||||
// Additions and removals render as small deltas; anything else restates the full list.
|
||||
if (changed.length > 0 || (added.length === 0 && removed.length === 0))
|
||||
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
||||
return [
|
||||
"The available skills have changed. This list supersedes the previous available skills list.",
|
||||
render(current),
|
||||
].join("\n")
|
||||
return [
|
||||
...(added.length === 0
|
||||
...(diff.added.length === 0
|
||||
? []
|
||||
: ["New skills are available in addition to those previously listed:", ...entries(added)]),
|
||||
...(removed.length === 0
|
||||
: ["New skills are available in addition to those previously listed:", ...entries(diff.added)]),
|
||||
...(diff.removed.length === 0
|
||||
? []
|
||||
: [
|
||||
`The following skills are no longer available and must not be used: ${removed.map((skill) => skill.name).join(", ")}.`,
|
||||
`The following skills are no longer available and must not be used: ${diff.removed.map((skill) => skill.name).join(", ")}.`,
|
||||
]),
|
||||
].join("\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,6 +153,32 @@ export function make<A>(source: Source<A>): SystemContext {
|
|||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyed three-way diff for list-shaped sources rendering delta updates.
|
||||
* `changed` compares two values sharing a key; entries equal under it are dropped.
|
||||
*/
|
||||
export function diffByKey<A>(
|
||||
previous: ReadonlyArray<A>,
|
||||
current: ReadonlyArray<A>,
|
||||
key: (value: A) => string,
|
||||
changed: (previous: A, current: A) => boolean,
|
||||
): {
|
||||
readonly added: ReadonlyArray<A>
|
||||
readonly removed: ReadonlyArray<A>
|
||||
readonly changed: ReadonlyArray<{ readonly previous: A; readonly current: A }>
|
||||
} {
|
||||
const currentKeys = new Set(current.map(key))
|
||||
const previousByKey = new Map(previous.map((value) => [key(value), value] as const))
|
||||
return {
|
||||
added: current.filter((value) => !previousByKey.has(key(value))),
|
||||
removed: previous.filter((value) => !currentKeys.has(key(value))),
|
||||
changed: current.flatMap((value) => {
|
||||
const before = previousByKey.get(key(value))
|
||||
return before === undefined || !changed(before, value) ? [] : [{ previous: before, current: value }]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/** Combines contexts in order and rejects duplicate source keys immediately. */
|
||||
export function combine(values: ReadonlyArray<SystemContext>): SystemContext {
|
||||
const sources = values.flatMap((value) => value[ContextTypeId])
|
||||
|
|
|
|||
|
|
@ -301,6 +301,39 @@ describe("SystemContext", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("diffs list values by key with a changed comparator", () =>
|
||||
Effect.sync(() => {
|
||||
const previous = [
|
||||
{ name: "effect", description: "Build with Effect" },
|
||||
{ name: "debugging", description: "Diagnose bugs" },
|
||||
{ name: "retired", description: "Old" },
|
||||
]
|
||||
const current = [
|
||||
{ name: "effect", description: "Build with Effect v4" },
|
||||
{ name: "debugging", description: "Diagnose bugs" },
|
||||
{ name: "writing", description: "Write prose" },
|
||||
]
|
||||
|
||||
expect(
|
||||
SystemContext.diffByKey(
|
||||
previous,
|
||||
current,
|
||||
(value) => value.name,
|
||||
(before, after) => before.description !== after.description,
|
||||
),
|
||||
).toEqual({
|
||||
added: [{ name: "writing", description: "Write prose" }],
|
||||
removed: [{ name: "retired", description: "Old" }],
|
||||
changed: [
|
||||
{
|
||||
previous: { name: "effect", description: "Build with Effect" },
|
||||
current: { name: "effect", description: "Build with Effect v4" },
|
||||
},
|
||||
],
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects duplicate source keys", () =>
|
||||
Effect.sync(() => {
|
||||
expect(() =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue