Compare commits

..
Author SHA1 Message Date
dc2b1b5b19 cmms-embed: ai-bridge apply — carry newly-placed symbols' (lib_symbols) defs
Some checks failed
wasm-build.yml / cmms-embed: ai-bridge apply — carry newly-placed symbols' (lib_symbols) defs (push) Failing after 0s
wasm-build.yml / cmms-embed: ai-bridge apply — carry newly-placed symbols' (lib_symbols) defs (release) Failing after 0s
release / meta (push) Has been cancelled
release / build (push) Has been cancelled
release / publish-wasm (push) Has been cancelled
release / deploy-demo (push) Has been cancelled
release / deploy-editor (push) Has been cancelled
release / Build all tools + KiCad e2e (Ubicloud) (push) Has been cancelled
The cmms-ai-editor-apply handler diffed the proposed sheet with docDelta and
pushed the item delta through applyDeltaToY. docDelta only diffs uuid-bearing
items; a (lib_symbols ...) definition has no uuid, so an AI proposal that
places a part the sheet has never carried landed the instance with no library
definition — KiCad drew an instance pointing at an undefined symbol (nothing).

applyToActiveBuffer now upserts the proposal's lib defs into kdoc_libsymbols
(upsert by lib id, never deletes — the CMMS's kicad_splice_elements always
splices into the existing block, so the proposal is existing ∪ additions) before
the item delta, mirroring kicad-binding.ts's own DOWN path
(upsertLibSymbolsToY(doc, defs, ORIGIN)). Deliberately not syncLayoutToY, which
would also LWW-reconcile non-item layout heads (root uuid, sheet_instances,
paper) from the proposal.

New ai-bridge.test.ts covers it (fails without the upsert: the placed symbol's
definition never reaches kdoc_libsymbols).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 14:51:35 +10:00
2 changed files with 120 additions and 0 deletions

View file

@ -0,0 +1,104 @@
import { describe, expect, it } from "vitest";
import * as Y from "yjs";
import { docToFile, docToY, kicadLibSymbolsMap, fileToDoc, yToDoc } from "@pcbjam/shared";
import { applyToActiveBuffer, readActiveBuffer, registerActiveCollabDoc } from "./ai-bridge";
// A symbol instance is only renderable with its `(lib_symbols …)` definition.
// `docDelta` (the apply path's item differ) only diffs uuid-bearing items, and a
// `(symbol "Device:R" …)` lib def has no uuid — so an AI proposal that places a
// part the sheet has never carried used to land the instance with no definition,
// and KiCad drew nothing. `applyToActiveBuffer` now upserts the proposal's lib
// defs into `kdoc_libsymbols` alongside the item delta.
const WIRE1 = "22222222-0000-0000-0000-000000000001";
const SYM1 = "44444444-0000-0000-0000-000000000001";
const SCH_WITHOUT_SYMBOL = `(kicad_sch
\t(version 20250114)
\t(generator "eeschema")
\t(generator_version "9.0")
\t(uuid "11111111-1111-1111-1111-111111111111")
\t(paper "A4")
\t(lib_symbols)
\t(wire (pts (xy 50.8 50.8) (xy 101.6 50.8)) (stroke (width 0) (type default)) (uuid "${WIRE1}"))
\t(sheet_instances (path "/" (page "1")))
)
`;
const SCH_WITH_SYMBOL = `(kicad_sch
\t(version 20250114)
\t(generator "eeschema")
\t(generator_version "9.0")
\t(uuid "11111111-1111-1111-1111-111111111111")
\t(paper "A4")
\t(lib_symbols
\t\t(symbol "Device:R" (pin_numbers (hide yes)) (pin_names (offset 0)) (exclude_from_sim no) (in_bom yes) (on_board yes)
\t\t\t(property "Reference" "R" (at 2.032 0 90) (effects (font (size 1.27 1.27))))
\t\t\t(property "Value" "R" (at 0 0 90) (effects (font (size 1.27 1.27))))
\t\t\t(symbol "R_0_1"
\t\t\t\t(rectangle (start -1.016 -2.54) (end 1.016 2.54) (stroke (width 0.254) (type default)) (fill (type none)))
\t\t\t)
\t\t\t(symbol "R_1_1"
\t\t\t\t(pin passive line (at 0 3.81 270) (length 1.27) (name "~" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))))
\t\t\t\t(pin passive line (at 0 -3.81 90) (length 1.27) (name "~" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))))
\t\t\t)
\t\t)
\t)
\t(wire (pts (xy 50.8 50.8) (xy 101.6 50.8)) (stroke (width 0) (type default)) (uuid "${WIRE1}"))
\t(symbol (lib_id "Device:R") (at 63.5 63.5 0) (unit 1) (exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)
\t\t(uuid "${SYM1}")
\t\t(property "Reference" "R1" (at 66.04 62.23 0) (effects (font (size 1.27 1.27)) (justify left)))
\t\t(property "Value" "10k" (at 66.04 64.77 0) (effects (font (size 1.27 1.27)) (justify left)))
\t\t(pin "1" (uuid "44444444-0000-0000-0000-0000000000a1"))
\t\t(pin "2" (uuid "44444444-0000-0000-0000-0000000000a2"))
\t\t(instances (project "rt" (path "/11111111-1111-1111-1111-111111111111" (reference "R1") (unit 1))))
\t)
\t(sheet_instances (path "/" (page "1")))
)
`;
function seed(src: string): Y.Doc {
const ydoc = new Y.Doc();
docToY(fileToDoc(src), ydoc);
return ydoc;
}
describe("ai-bridge applyToActiveBuffer", () => {
it("carries a newly-placed symbol's (lib_symbols) definition, not just the instance", () => {
const ydoc = seed(SCH_WITHOUT_SYMBOL);
registerActiveCollabDoc(ydoc, "board.kicad_sch");
try {
expect(kicadLibSymbolsMap(ydoc).get("Device:R")).toBeUndefined();
applyToActiveBuffer(SCH_WITH_SYMBOL);
const def = kicadLibSymbolsMap(ydoc).get("Device:R");
expect(def).toBeDefined();
expect(def).toContain("R_0_1");
// and the instance item itself landed via the normal item delta
const out = docToFile(yToDoc(ydoc));
expect(out).toContain(SYM1);
expect(out).toContain('(lib_id "Device:R")');
} finally {
registerActiveCollabDoc(null);
}
});
it("is a no-op on lib_symbols when the proposal adds no new definitions", () => {
const ydoc = seed(SCH_WITH_SYMBOL);
registerActiveCollabDoc(ydoc, "board.kicad_sch");
try {
const before = kicadLibSymbolsMap(ydoc).get("Device:R");
expect(before).toBeDefined();
// same doc, wire nudged — no new parts
applyToActiveBuffer(SCH_WITH_SYMBOL.replace("101.6 50.8", "127 50.8"));
expect(kicadLibSymbolsMap(ydoc).get("Device:R")).toBe(before);
expect(readActiveBuffer()).toContain("127 50.8");
} finally {
registerActiveCollabDoc(null);
}
});
});

View file

@ -28,6 +28,8 @@ import {
docDelta,
docToFile,
fileToDoc,
libSymbolsFromLayout,
upsertLibSymbolsToY,
yToDoc,
type KicadDoc,
} from "@pcbjam/shared";
@ -82,6 +84,20 @@ export function applyToActiveBuffer(newSource: string): void {
const prev = yToDoc(activeDoc);
const next = fileToDoc(newSource);
const delta = docDelta(prev, next);
// `docDelta` only diffs uuid-bearing items — a `(lib_symbols …)` def has no
// uuid, so a proposal that places a symbol the sheet has never carried adds
// the instance item but drops its library definition, leaving KiCad an
// instance pointing at an undefined symbol (renders nothing). Push the
// proposal's lib defs into the kdoc_libsymbols map FIRST (upsert by lib id,
// never deletes — the server always splices into the existing block, so this
// is existing ∪ additions), so the items observer's `deltaToItemsWire` can
// prefix each new instance with its `(lib_symbols …)` on the way to the
// editor — exactly what `kicad-binding.ts`'s own DOWN path does with
// `upsertLibSymbolsToY(doc, defs, ORIGIN)`. Deliberately NOT `syncLayoutToY`:
// that also reconciles non-item layout heads (root `uuid`, `sheet_instances`,
// `paper`…) wholesale-LWW from the proposal, which would corrupt the sheet's
// identity if the server's spliced preamble deviates at all.
upsertLibSymbolsToY(activeDoc, libSymbolsFromLayout(next.layout, next.items), APPLY_ORIGIN);
applyDeltaToY(activeDoc, delta, APPLY_ORIGIN);
}