kicad-validity B3+C+S: revert toast, corpus lint, .kicad_mod tier
B3 (editor): kicad-binding observes kdoc_meta.revertNonce (deduped, like seedNonce) → DOC_REVERTED_EVENT → WasmTool doc-reverted-toast (15 s, data-testid for the deferred e2e check). C: tests/tools/corpus-lint.ts (npm run corpus:lint) — kicad_tools --lint over the fixture corpora raw + shared-codec round-trips (docToFile(fileToDoc(x))); the E3/wrapInBoardEnvelope-class gate. Skips when output/kicad_tools.js isn't built; CI step lands when kicad_tools joins the CI app set. 7 fixtures / 7 round-trips green. S: --lint + --resave gain the .kicad_mod tier (pcbToolsLintFootprint / pcbToolsResaveFootprint; writer needs CTL_FOR_LIBRARY or the board flags strip the (version) header). Verified: qa footprint round-trip lints clean, resave stamps 20260206, garbage exits 1. Also bumps pcbjam-shared (mergeYUpdates for the bisect pass). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLua64PCVwkQ1hpWdaf1Gm
This commit is contained in:
parent
b18b0a6a08
commit
fb2aa2759d
8 changed files with 323 additions and 7 deletions
|
|
@ -67,6 +67,7 @@ import {
|
|||
type FollowTarget,
|
||||
} from "@/wasm/collab/follow-user";
|
||||
import { startCrossAppPresence, type CrossAppHandle } from "@/wasm/collab/cross-app";
|
||||
import { DOC_REVERTED_EVENT } from "@/wasm/collab/kicad-binding";
|
||||
import {
|
||||
createComments,
|
||||
hasCommentsBridge,
|
||||
|
|
@ -790,6 +791,9 @@ export function WasmTool({
|
|||
// A collaborator updated library items that are PLACED in the open document
|
||||
// (LIB_ITEM_UPDATED_EVENT) — placed copies keep the previous version, so warn.
|
||||
const [libUpdate, setLibUpdate] = React.useState<string | null>(null);
|
||||
// The backend rolled this document back to its last valid state
|
||||
// (kicad-validity 0001 — DOC_REVERTED_EVENT from the collab binding).
|
||||
const [docReverted, setDocReverted] = React.useState<string | null>(null);
|
||||
// Eager whole-library idb→wasm load in flight (the ~tens-of-seconds fat-load on
|
||||
// first chooser/editor open). Drives a full-cover overlay so the freeze reads as
|
||||
// "loading, just slow" rather than a hang. Null when idle; `done/total` count the
|
||||
|
|
@ -856,14 +860,23 @@ export function WasmTool({
|
|||
`placed copies keep the previous version until updated from the library.`,
|
||||
);
|
||||
};
|
||||
const onDocReverted = (e: Event) => {
|
||||
const d = (e as CustomEvent<{ reason?: string; at?: string }>).detail;
|
||||
setDocReverted(
|
||||
`This document was rolled back to its last valid state — invalid content ` +
|
||||
`was detected${d?.reason ? ` (${d.reason})` : ""}. Recent edits may have been undone.`,
|
||||
);
|
||||
};
|
||||
window.addEventListener(LIB_BUSY_EVENT, onBusy);
|
||||
window.addEventListener(LIB_ERROR_EVENT, onError);
|
||||
window.addEventListener(LIB_ITEM_UPDATED_EVENT, onItemUpdated);
|
||||
window.addEventListener(DOC_REVERTED_EVENT, onDocReverted);
|
||||
return () => {
|
||||
clearTimeout(busyTimer);
|
||||
window.removeEventListener(LIB_BUSY_EVENT, onBusy);
|
||||
window.removeEventListener(LIB_ERROR_EVENT, onError);
|
||||
window.removeEventListener(LIB_ITEM_UPDATED_EVENT, onItemUpdated);
|
||||
window.removeEventListener(DOC_REVERTED_EVENT, onDocReverted);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
@ -881,6 +894,13 @@ export function WasmTool({
|
|||
return () => clearTimeout(t);
|
||||
}, [libUpdate]);
|
||||
|
||||
// Auto-dismiss the doc-reverted toast (longest — the user should see it).
|
||||
React.useEffect(() => {
|
||||
if (!docReverted) return;
|
||||
const t = setTimeout(() => setDocReverted(null), 15_000);
|
||||
return () => clearTimeout(t);
|
||||
}, [docReverted]);
|
||||
|
||||
// Full-library eager load overlay. The fat-load fires one loading:true/false
|
||||
// pair PER library (222 on the full set), and between them the C++ side parses
|
||||
// with the main thread blocked. Show immediately on `true`, and only hide after
|
||||
|
|
@ -1647,6 +1667,18 @@ export function WasmTool({
|
|||
</button>
|
||||
)}
|
||||
|
||||
{/* Backend rolled this doc back to the last valid state (kicad-validity). */}
|
||||
{docReverted && (
|
||||
<button
|
||||
data-testid="doc-reverted-toast"
|
||||
className="absolute left-1/2 top-3 z-40 max-w-md -translate-x-1/2 rounded bg-orange-950/95 px-3 py-2 text-center text-xs text-orange-100 shadow-lg ring-1 ring-orange-500/40"
|
||||
onClick={() => setDocReverted(null)}
|
||||
title="Dismiss"
|
||||
>
|
||||
{docReverted}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!effectiveChromeHidden && (
|
||||
<div className="absolute bottom-0 left-0 right-0 z-20">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import {
|
|||
wireItemUuids,
|
||||
wireLibSymbols,
|
||||
Y_KDOC_META,
|
||||
Y_KDOC_REVERT_AT,
|
||||
Y_KDOC_REVERT_NONCE,
|
||||
Y_KDOC_REVERT_REASON,
|
||||
Y_KDOC_SEED_NONCE,
|
||||
ydocHasState,
|
||||
ydocSexprVersion,
|
||||
|
|
@ -44,6 +47,12 @@ import { clog, cwarn } from "./debug";
|
|||
* fake editor bridge (kicad-binding.test.ts).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Window event fired when the backend rolled this doc back to its last valid
|
||||
* state (kicad-validity 0001 B3). detail: { reason?: string; at?: string }.
|
||||
*/
|
||||
export const DOC_REVERTED_EVENT = "pcbjam:doc-reverted";
|
||||
|
||||
/** The v2 per-item s-expr bridge (Stage C C++ contract), runtime-adapted. */
|
||||
export interface KicadItemsBridge {
|
||||
/** Full current model as an all-`added` ItemsWireDelta JSON. */
|
||||
|
|
@ -192,6 +201,31 @@ export function bindKicadCollab(
|
|||
};
|
||||
items.observeDeep(observer);
|
||||
|
||||
// Validity-revert notice (kicad-validity 0001 B3): the backend stamps
|
||||
// kdoc_meta.revertNonce when it rolls the doc back to the last valid state
|
||||
// (the content itself arrives through the normal item sync above). Watched
|
||||
// like seedNonce; surfaced as a window event for the shell's toast. The
|
||||
// nonce is deduped so a reconnect replaying the same marker stays silent.
|
||||
const revMeta = doc.getMap(Y_KDOC_META);
|
||||
let lastRevertNonce = revMeta.get(Y_KDOC_REVERT_NONCE);
|
||||
const onRevertMeta = () => {
|
||||
const nonce = revMeta.get(Y_KDOC_REVERT_NONCE);
|
||||
if (nonce === undefined || nonce === lastRevertNonce) return;
|
||||
lastRevertNonce = nonce;
|
||||
clog("doc reverted by backend:", revMeta.get(Y_KDOC_REVERT_REASON));
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(DOC_REVERTED_EVENT, {
|
||||
detail: {
|
||||
reason: revMeta.get(Y_KDOC_REVERT_REASON),
|
||||
at: revMeta.get(Y_KDOC_REVERT_AT),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
revMeta.observe(onRevertMeta);
|
||||
|
||||
function seed(seedDoc?: KicadDoc, opts?: { editorMatchesDoc?: boolean }): void {
|
||||
seeded = true; // open the UP gate; everything below runs synchronously
|
||||
// `ydocHasState` (meta + layout + items), NOT `items.size`: a populated
|
||||
|
|
@ -344,6 +378,7 @@ export function bindKicadCollab(
|
|||
detachSeedArbitration?.();
|
||||
detachSeedArbitration = undefined;
|
||||
items.unobserveDeep(observer);
|
||||
revMeta.unobserve(onRevertMeta);
|
||||
},
|
||||
items,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue