Skip to content

Commit 349e59f

Browse files
committed
fix(artifacts): match drifted update id by normalized key before latest-artifact fallback
Addresses review: with multiple artifacts, a case/whitespace-drifted id (e.g. 'App' for 'app') was attached to the most-recently-created artifact instead of the one it meant. Now try a normalized (trim+lowercase) id match first; fall back to the latest only when there's no unambiguous normalized match.
1 parent f266f82 commit 349e59f

2 files changed

Lines changed: 32 additions & 5 deletions

File tree

src/lib/utils/artifacts.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,22 @@ describe("collectArtifacts", () => {
330330
expect(registry.byMessageOp.get("m3:0")).toEqual({ identifier: "second", version: 2 });
331331
});
332332

333+
it("prefers a normalized identifier match over the latest artifact for drifted ids", () => {
334+
const registry = collectArtifacts([
335+
msg("m1", `<artifact identifier="app" type="html" title="App">alpha</artifact>`),
336+
msg("m2", `<artifact identifier="chart" type="html" title="Chart">bravo</artifact>`),
337+
msg(
338+
"m3",
339+
`<artifact identifier="App" type="update"><old_str>alpha</old_str><new_str>gamma</new_str></artifact>`
340+
),
341+
]);
342+
// "App" drifts from "app" by case, so it links there — not the latest ("chart")
343+
expect(registry.artifacts.get("app")?.versions).toHaveLength(2);
344+
expect(registry.artifacts.get("app")?.versions[1]?.content).toBe("gamma");
345+
expect(registry.artifacts.get("chart")?.versions).toHaveLength(1);
346+
expect(registry.byMessageOp.get("m3:0")).toEqual({ identifier: "app", version: 2 });
347+
});
348+
333349
it("applies streaming update pairs progressively", () => {
334350
const registry = collectArtifacts(
335351
[

src/lib/utils/artifacts.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,11 @@ export function artifactOpKey(messageId: Message["id"], opIndex: number): string
329329
return `${messageId}:${opIndex}`;
330330
}
331331

332+
/** Case/whitespace-insensitive identifier key, for tolerant update linking. */
333+
function normalizeIdentifier(id: string): string {
334+
return id.trim().toLowerCase();
335+
}
336+
332337
/**
333338
* Walk the active conversation path and fold artifact operations into
334339
* versioned artifacts. Updates apply onto the latest version of the same
@@ -395,12 +400,18 @@ export function collectArtifacts(
395400
// update op
396401
// Models sometimes rename the identifier when editing (e.g. green-button ->
397402
// blue-button) or drift its case/whitespace, which would orphan the update as a
398-
// dead "couldn't be linked" card. An update is by definition an edit to existing
399-
// content, so when the id doesn't match an existing artifact, link to the
400-
// most-recently-created one (the Map preserves insertion order). Only leave a
401-
// disabled card when there's genuinely nothing to link to.
403+
// dead "couldn't be linked" card. When the id doesn't match an existing artifact,
404+
// first try a normalized (case/whitespace-insensitive) match so a drifted id hits
405+
// the artifact it meant; otherwise fall back to the most-recently-created one (an
406+
// update is by definition an edit to existing content, and the Map preserves
407+
// insertion order). Only leave a disabled card when there's nothing to link to.
402408
if (!artifact && artifacts.size > 0) {
403-
artifact = [...artifacts.values()].at(-1);
409+
const wanted = normalizeIdentifier(op.identifier);
410+
const normalizedMatches = [...artifacts.values()].filter(
411+
(a) => normalizeIdentifier(a.identifier) === wanted
412+
);
413+
artifact =
414+
normalizedMatches.length === 1 ? normalizedMatches[0] : [...artifacts.values()].at(-1);
404415
}
405416
const base = artifact?.versions.at(-1);
406417
if (!artifact || !base) {

0 commit comments

Comments
 (0)