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")
|
["<mcp_instructions>", ...entries(servers), "</mcp_instructions>"].join("\n")
|
||||||
|
|
||||||
const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary>) => {
|
const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary>) => {
|
||||||
const names = new Set(current.map((server) => server.server))
|
const diff = SystemContext.diffByKey(
|
||||||
const previousByName = new Map(previous.map((server) => [server.server, server]))
|
previous,
|
||||||
const added = current.filter((server) => !previousByName.has(server.server))
|
current,
|
||||||
const removed = previous.filter((server) => !names.has(server.server))
|
(server) => server.server,
|
||||||
const changed = current.filter((server) => {
|
(before, after) => before.instructions !== after.instructions,
|
||||||
const before = previousByName.get(server.server)
|
)
|
||||||
return before !== undefined && before.instructions !== server.instructions
|
|
||||||
})
|
|
||||||
// Additions and removals render as small deltas; anything else restates the full list.
|
// 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 [
|
return [
|
||||||
"The available MCP server instructions have changed. This list supersedes the previous one.",
|
"The available MCP server instructions have changed. This list supersedes the previous one.",
|
||||||
render(current),
|
render(current),
|
||||||
].join("\n")
|
].join("\n")
|
||||||
return [
|
return [
|
||||||
...(added.length === 0
|
...(diff.added.length === 0
|
||||||
? []
|
? []
|
||||||
: ["New MCP server instructions are available in addition to those previously listed:", ...entries(added)]),
|
: ["New MCP server instructions are available in addition to those previously listed:", ...entries(diff.added)]),
|
||||||
...(removed.length === 0
|
...(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")
|
].join("\n")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,28 +29,26 @@ const render = (references: ReadonlyArray<typeof Summary.Type>) =>
|
||||||
].join("\n")
|
].join("\n")
|
||||||
|
|
||||||
const update = (previous: ReadonlyArray<typeof Summary.Type>, current: ReadonlyArray<typeof Summary.Type>) => {
|
const update = (previous: ReadonlyArray<typeof Summary.Type>, current: ReadonlyArray<typeof Summary.Type>) => {
|
||||||
const names = new Set(current.map((reference) => reference.name))
|
const diff = SystemContext.diffByKey(
|
||||||
const previousByName = new Map(previous.map((reference) => [reference.name, reference]))
|
previous,
|
||||||
const added = current.filter((reference) => !previousByName.has(reference.name))
|
current,
|
||||||
const removed = previous.filter((reference) => !names.has(reference.name))
|
(reference) => reference.name,
|
||||||
const changed = current.filter((reference) => {
|
(before, after) => before.path !== after.path || before.description !== after.description,
|
||||||
const before = previousByName.get(reference.name)
|
)
|
||||||
return before !== undefined && (before.path !== reference.path || before.description !== reference.description)
|
|
||||||
})
|
|
||||||
// Additions and removals render as small deltas; anything else restates the full list.
|
// 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 [
|
return [
|
||||||
"The available project references have changed. This list supersedes the previous reference list.",
|
"The available project references have changed. This list supersedes the previous reference list.",
|
||||||
render(current),
|
render(current),
|
||||||
].join("\n")
|
].join("\n")
|
||||||
return [
|
return [
|
||||||
...(added.length === 0
|
...(diff.added.length === 0
|
||||||
? []
|
? []
|
||||||
: ["New project references are available in addition to those previously listed:", ...entries(added)]),
|
: ["New project references are available in addition to those previously listed:", ...entries(diff.added)]),
|
||||||
...(removed.length === 0
|
...(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")
|
].join("\n")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,28 +31,26 @@ const render = (skills: ReadonlyArray<Summary>) =>
|
||||||
].join("\n")
|
].join("\n")
|
||||||
|
|
||||||
const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary>) => {
|
const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary>) => {
|
||||||
const names = new Set(current.map((skill) => skill.name))
|
const diff = SystemContext.diffByKey(
|
||||||
const previousByName = new Map(previous.map((skill) => [skill.name, skill]))
|
previous,
|
||||||
const added = current.filter((skill) => !previousByName.has(skill.name))
|
current,
|
||||||
const removed = previous.filter((skill) => !names.has(skill.name))
|
(skill) => skill.name,
|
||||||
const changed = current.filter((skill) => {
|
(before, after) => before.description !== after.description,
|
||||||
const before = previousByName.get(skill.name)
|
)
|
||||||
return before !== undefined && before.description !== skill.description
|
|
||||||
})
|
|
||||||
// Additions and removals render as small deltas; anything else restates the full list.
|
// 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 [
|
return [
|
||||||
"The available skills have changed. This list supersedes the previous available skills list.",
|
"The available skills have changed. This list supersedes the previous available skills list.",
|
||||||
render(current),
|
render(current),
|
||||||
].join("\n")
|
].join("\n")
|
||||||
return [
|
return [
|
||||||
...(added.length === 0
|
...(diff.added.length === 0
|
||||||
? []
|
? []
|
||||||
: ["New skills are available in addition to those previously listed:", ...entries(added)]),
|
: ["New skills are available in addition to those previously listed:", ...entries(diff.added)]),
|
||||||
...(removed.length === 0
|
...(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")
|
].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. */
|
/** Combines contexts in order and rejects duplicate source keys immediately. */
|
||||||
export function combine(values: ReadonlyArray<SystemContext>): SystemContext {
|
export function combine(values: ReadonlyArray<SystemContext>): SystemContext {
|
||||||
const sources = values.flatMap((value) => value[ContextTypeId])
|
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", () =>
|
it.effect("rejects duplicate source keys", () =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue