feat: blank-node skolemization, MCP hardening, demo fixes#11
Merged
Conversation
added 30 commits
May 8, 2026 16:44
isAiStreaming() rewritten with four UI-agnostic signals: 1. input disabled/aria-busy (textarea UIs) 2. visible spinner by class name 3. stop/abort button by aria-label OR exact text content 4. MutationObserver fallback — DOM silence for 1.5 s = done; catches icon-only stop buttons and thinking phases where none of the above signals fire (e.g. OWUI qwen3) Removed #send-message-button dependency and type=submit fallback that matched all OWUI buttons and caused false negatives during the qwen3 thinking phase. help() now opens with a full SILENTLY IGNORED format list covering every common wrong format, including the method="toolName" mistake qwen3 made on first attempt.
Adds everything needed to run an interactive OWUI relay session via Playwright MCP browser without external Node processes (BroadcastChannel must share the MCP browser process). - .playwright/: fresh-setup, send-starter, send-task, send-pizza send-task is a generic injection wrapper; send-pizza is a thin wrapper over it - e2e/demo-openwebui-pizza.spec.ts: recorded demo spec with silent-ignore warning in SYSTEM_PROMPT - playwright.openwebui.config.ts: Playwright config for OWUI tests - public/demo-stage-owui.html: side-by-side OWUI + Ontosphere stage - scripts/owui-auth.mjs: saves auth cookie to .playwright/owui-auth.json - docs/owui-relay-session.md: comprehensive session setup guide - CLAUDE.md: pointer to the session guide - package.json: demo:owui:auth and demo:owui:video scripts - .gitignore: exclude owui-auth.json (contains session tokens)
…akes Make isAiStreaming() fully generic — no chat-UI-specific selectors. Now uses four layered signals: textarea disabled/aria, visible spinner classes, stop-button aria-label/text, and MutationObserver DOM-silence fallback (STREAM_QUIET_MS=4000ms). Covers qwen3:8b icon-only stop button and any UI without standard spinner markup. Also add WRONG/RIGHT examples for queryGraph (sparql≠query) and setViewMode (mode≠viewMode) to help() — both consistently misused by qwen3.
MutationObserver fired mid-stream causing race conditions with thinking models (qwen3). Fire-on-idle polls every 500ms, waits for isAiStreaming() = false, reads the complete page text, extracts all tool calls at once. - relay: replace MutationObserver block with idlePoll() (500ms interval) - relay: pre-seed dispatchedSigs before poll starts to skip INSTR examples - relay: isAiStreaming() now checks spinners, stop-word buttons, DOM quiet - playwright: send-starter uses Good Response button (not __vgIsStreaming) - playwright: fresh-setup selects mistral-small3.1 (no thinking blocks)
…solutions discoverability to CLAUDE.md
…o bookmarklet plugin doSubmit() deadlocked: content injection causes DOM mutations → isAiStreaming() returns true for STREAM_QUIET_MS (4s) → submit poll blocks. By the time injectResult is called we already confirmed model idle via fire-on-idle; no need to re-check. vite-plugin-bookmarklet: addWatchFile so virtual module invalidates on source change.
…RESPONSE] UUID prefix insertFromPaste (appends) was promoted to Path 1 to sync Svelte for btn.click(). But OWUI pre-fills TipTap with [RESPONSE] <uuid> internal state in some conditions; append produces [RESPONSE] UUID + result text, model echoes the UUID. setContent(text, true) REPLACES all editor content. Enter-based submit reads TipTap state directly so Svelte async sync no longer matters. Path order: 1. setContent / raw PM dispatch (replace — correct for any pre-filled state) 2. insertFromPaste (append — fallback for non-TipTap contenteditable) 3. insertText (last resort)
…textarea Clear TipTap before insertFromPaste so [RESPONSE] UUID pre-fill is gone before append — paste to empty = clean insert, Svelte syncs, button enabled. Enter fires only for textarea UIs; TipTap (OWUI) maps Enter to new paragraph not submit — btn.click() is the correct path after Svelte syncs via paste.
hasContent fired submitInput on the first synchronous poll — before setContent's microtask had synced Svelte's prompt store. Button was still disabled → no click → silent fail or premature fire with stale content. doSubmit now polls btnEnabled only, giving the ~50 ms microtask queue time to flush. insertFromPaste (and the pre-clear step) removed; setContent atomically replaces any [RESPONSE] UUID pre-fill and is the sole inject path.
…nt submit
Remove all fallback injection paths (raw PM dispatch, insertFromPaste,
insertText). One path: setContent(text, true) to atomically replace content,
tiptap.on('transaction') to submit once Svelte's onTransaction sync completes.
300 ms safety timeout handles edge cases where the event never fires.
Eliminates the doSubmit polling loop and the entire multi-path injection chain
that caused UUID prefix bugs and premature submit races.
Two fixes:
1. Text-stability fallback in idlePoll: track innerText change time
separately from DOM mutation rate. If text unchanged for STREAM_QUIET_MS,
consider idle regardless of background DOM mutations. Fixes FhGenie where
continuous UI updates (progress indicator, animations) kept isAiStreaming()
permanently true, blocking all tool-call dispatch.
2. Delta extraction: process only text.slice(lastIdleText.length) per poll
instead of full page text + global dispatchedSigs. Same call in a later
turn fires again ("call it again" works). Per-turn Set still deduplicates
duplicate calls within a single response.
…utton isAiStreaming() signal 3: add SVG path prefix matching for icon-only stop buttons. FhGenie (Fluent UI) replaces the send arrow with a Dismiss24Regular X icon (path 'M8.22 8.22') during generation — no aria-label or text, so stop-word matching missed it. submitInput(): extend send button heuristic to also match class name containing 'send' or 'submit'. FhGenie's button has class _questionInputSendButton_* with no matching text or aria-label.
isAiStreaming() rewrite: - Primary signal: find send/submit button (by id, tree-climb, aria/text/class) - If send button found: check for X icon (M8.22 SVG path = Fluent UI Dismiss, FhGenie's stop affordance) or disabled-with-content (OWUI pattern) - If send button not found: fallback to aria-disabled/busy, spinners, stop words - Remove DOM mutation rate signal (signal 4) — caused permanent false positives on FhGenie due to background UI updates idlePoll rewrite: - Remove text-stability complexity (no longer needed) - On idle: full page text scan with global dispatchedSigs - Pre-seed dispatchedSigs at inject to skip pre-existing calls - Clean and predictable: one mechanism, no delta/stability bookkeeping
React re-renders asynchronously after the native value setter + input event. Calling submitInput() synchronously hits a still-disabled button (React hasn't re-rendered yet) and Enter fires before the value is in React state — both ignored. 50ms timeout lets React flush its state update before clicking.
Injecting while OWUI transitions out of streaming state causes a race: setContent fires before the editor accepts input, pasting lands too early. Poll isAiStreaming() (up to 10s) before calling setContent. Safe now that signal 4 (DOM mutation rate) is removed — content injection no longer triggers false positives in isAiStreaming().
… input idlePoll was firing on every idle tick including while user types or pastes. The starter prompt contains JSON-RPC example calls — relay dispatched them immediately on paste before the AI ever responded. Track prevStreaming: only dispatch on the single tick where streaming just ended (true→false transition). User messages arrive while always-idle so the transition never fires for them.
getPageText() uses a TreeWalker to collect body text skipping the input element subtree. Tool calls typed or injected into the input (INSTR examples, relay result text) can never be dispatched. All three scan sites updated: idlePoll, pre-seed, and waitForIdle.
getPageText() now strips the chat input's current content via string subtraction — prevents relay from dispatching tool calls the user typed or pasted into the input field. prevStreaming transition guard removed: getPageText() is the sole protection against dispatching user content, so the streaming→idle edge-detect is redundant and prevents legitimate re-dispatch after multi-turn conversations.
…T walker String subtraction was fragile — TipTap reformats pasted content (strips backticks, changes whitespace) so inp.innerText != body.innerText substring. The previous TreeWalker used SHOW_TEXT only: FILTER_REJECT on a text node is treated as FILTER_SKIP (no subtree effect), so the whole input was included. With SHOW_ELEMENT|SHOW_TEXT, FILTER_REJECT on the input element correctly skips its entire subtree. Textarea value is never in body.innerText, so TEXTAREA inputs skip the walker entirely.
Replace transaction-event+setTimeout(300) submit trigger with a waitSubmit loop that polls findSendButton() every 100ms, requiring 2 consecutive enabled ticks before clicking. Also simplify isAiStreaming() send-button branch: any disabled state means not ready, dropping the "disabled AND has content" heuristic that wrongly returned idle during model thinking with empty input. Together these ensure: - Pre-paste (waitReady): blocks while button is disabled (thinking phase) - Post-paste (waitSubmit): waits for Svelte to re-enable the button after setContent, not for the TipTap transaction which fires before the framework has flushed its render cycle Co-Authored-By: Thomas Hanke <thomas.hanke@iwm.fraunhofer.de>
…pty input FhGenie disables its send button when the input is empty (not just during generation). The previous change (disabled→streaming) broke FhGenie dispatch. New logic for the disabled branch: - enabled → idle (return false immediately) - disabled + has content → streaming (OWUI pattern) - disabled + empty → fall through to spinner/aria signals OWUI thinking is caught by the [class*="thinking"] spinner selector. FhGenie idle (disabled, empty, no spinner) correctly returns false.
… dispatch isAiStreaming() send-button branch: revert fallthrough for disabled+empty. FhGenie disables the send button when input is empty (not just during generation), and its Stream-mode toolbar elements match [class*="streaming"], causing the fallback spinner check to return true indefinitely. Correct logic: disabled+empty = idle (return false directly, like original). OWUI during generation replaces the send button with a stop button, so findSendButton() returns null and the fallback stop-word/spinner path handles it. idlePoll: require 2 consecutive idle ticks (1 s stable) before extracting. Prevents user starter-prompt examples from being dispatched in the brief window (~200ms) between user submit and model starting to stream.
Svelte needs more time to flush state after setContent and enable the send button reliably. 2 ticks (200ms) was sometimes too short.
Adds getAssistantText() that queries [data-message-author-role="assistant"] (OWUI), [data-role="assistant"], and aria-log fallbacks to restrict getPageText() to AI responses only. Prevents dispatching tool-call examples embedded in the user's starter prompt before the AI has responded.
Replaces the stableTicks heuristic with a loop that calls setContent, waits 600ms for OWUI's post-stream annotation phase to complete, then polls every 300ms checking isAiStreaming() before clicking submit. Success is detected by TipTap editor.isEmpty — a cleared input means OWUI accepted the message, no fixed tick count needed.
- relayBridge: log tool results/errors to console; fix unknown-tool path to emit console.error; add runLayout to toastLabel switch; rewrite buildCanvasSummary to use getNodes/getLinks instead of removed getGraphState - .playwright/pizza-demo-setup.js: new — bootstrap OWUI relay session with format INSTR and Socratic Turn 0 starter - .playwright/turn-driver.js: new — drive T1–T6 Socratic questions via __vgIsStreaming idle detection (replaces Good Response button polling) - .playwright/session-log-start.sh: new — persistent session log aggregator - fresh-setup.js / send-starter.js / send-pizza.js: model corrected to qwen3:4b - e2e/demo-pizza-socratic.spec.ts: new demo video spec — operator asks Socratic questions, model discovers OWL classes → subClassOf → hasPart → layout - docs/demo-scripts/pizza-ontology.md: recording guide with turn-by-turn expected tool calls and fallback nudges - .gitignore: add debug.png
4 MB mp4 / 6 MB webm — Socratic pizza ontology session, 7 turns, full class hierarchy + hasPart links + dagre-tb layout + introspection.
- demo-openwebui-socratic.spec.ts: runs full pizza session (T0-T6) live with qwen3:4b via OWUI relay; before/after caption overlays per turn; relay injected via cross-frame fetch from Ontosphere iframe - playwright.openwebui.config.ts: fix outputDir to test-results/demo so collect-demo-videos.mjs can find and convert the video to MP4 - docs/demo-scripts/pizza-ontology.md: add both reproduction paths (automated spec vs interactive MCP session), caption table, fallback nudges
Both specs now cover all 17 turns through OWL-RL reasoning and type adoption:
T0 root classes (setViewMode tbox + addNode ×3)
T1 owl:disjointWith between root classes
T2 ThinAndCrispyBase + DeepPanBase subclasses
T3 NamedPizza + Margherita/AmericanHot/FruttiDiMare
T4 CheeseTopping/MeatTopping/VegetableTopping/FishTopping categories
T5 pairwise topping disjointness
T6 7 leaf ingredient classes
T7 hasTopping + hasBase with domain/range
T8 isToppingOf + isBaseOf inverse properties
T9 owl:equivalentClass someValuesFrom restrictions via loadRdf
T10 switch to ABox, add untyped pizza1/pizza2/pizza3
T11 pizza1 (Margherita) with mozz1/tom1/thin1 individuals
T12 pizza2 (AmericanHot) with pep1/mozz2/olive1/deep1
T13 pizza3 (FruttiDiMare) with anch1/garlic1/thin2
T14 runReasoning({}) — materialise inferred triples
T15 focusNode/expandNode pizza1 → classified as Margherita
T16 focusNode/expandNode mozz1 → inferred as PizzaTopping
- demo-openwebui-socratic: extended INSTR includes runReasoning, loadRdf,
setViewMode, expandAll, focusNode, expandNode with full IRI reference
- demo-pizza-socratic: scripted version with same 17-turn arc, same endpoint
- pizza-demo-setup.js: updated INSTR + Turn 0 starts with TBox view
- turn-driver.js: updated to T1-T16 full arc
…etection isAiStreaming() gives false negatives in newer OWUI — send button stays enabled during generation so button-state detection fires too early. New approach: poll document.body.innerText.length (growing = active) plus relay-idle (callQueue===0 && !isProcessing). waitQuiet requires 10 s of continuous stability from both signals before typing the next question. waitIdle uses the same signals with a 3 s window for SEED/INSTR phases. Post-INSTR sleep bumped from 1 s to 3 s — help() manifest is large and the model needs time to finish reading it before the first Socratic turn fires.
…ixes
- Use complete README starter prompt typed via keyboard (model calls help() itself);
removes SEED+INSTR split that caused format confusion
- T1: explicit rdfs:subClassOf direction + "no other triples" + ex: prefix reminder
- T4: directive hasPart ObjectProperty creation ("Add it to the canvas now") to
prevent prose-only response deferring the property to T7
- T7: "pizza individual" subject + "only the hasPart object property" + "no other
properties" to prevent class-level subjects and wrong property substitutions
- help() manifest: ⚠️ example-call execution warning + GUIDED SESSION rule
suppressing suggestOntologiesForTask and loadOntology in guided flows
- turn-driver.js: replace unreliable isAiStreaming() with content-length growth +
relay-idle (callQueue+isProcessing) idle detection; add 10s waitQuiet before inject
- Add demo-restart.sh utility for clean MCP browser session restart
- Rebuild openwebui-socratic video (88MB, full T0-T9 run)
- Add openwebui-pizza video from same build run
Used old setSystemPrompt approach, qwen3:8b, hardcoded pizza.owl# IRIs, and addLink (wrong tool). Replaced by demo-openwebui-socratic.spec.ts.
…s deadline pizza-demo-setup.js step 4 now uses content-length + __vgIsRelayIdle (same as turn-driver.js) instead of the unreliable isAiStreaming(). waitQuiet in spec and turn-driver now accepts maxMs (default 45s) — prevents OWUI background updates (thinking dots, timestamps) from resetting the 10s silence timer indefinitely and stalling the run.
T1: add 'both edges required — do not stop after just one' to reliably get both base→Pizza and topping→Pizza subClassOf edges in the same response. T7: steer model away from reusing TBox class nodes as ABox individuals. Fresh IRIs (ex:MyCrust, ex:MyCheese) typed as third-level variety classes let the cax-sco subClass chain inference run (variety→building block→Pizza) and make T10 analysis more meaningful.
… addNode typeIri
T4: explicitly name 'rdfs:domain and rdfs:range — not owl:domain or owl:range'.
OWL-RL prp-dom/prp-rng rules only match rdfs: predicates; owl: variants are
invisible to the reasoner and break all domain/range inference silently.
T7: (1) hasPart must go pizza→part (pizza is subject), not part→pizza;
(2) use addNode(typeIri=varietyClass) for part type assertions to avoid
the rdfs:type vs rdf:type namespace confusion that occurs with addTriple.
- pizza-demo-setup.js: reload Ontosphere before each session to clear graph state left by previous interactive runs - T3: explicit rdfs:subClassOf direction (child=subject, parent=object) to prevent model from reversing the hierarchy - T6: require distinct IRI for pizza individual to prevent model from reusing the ex:Pizza class node - T7: use full rdf:type IRI in addTriple step instructions instead of addNode typeIri; prevents model using rdfs:subClassOf by mistake - T9: explicit getNodeDetails on individual not the class node - T10: force getNodeDetails tool call instead of text-only response Result: MyPizza gets ex:Pizza via prp-domain; MyCrust/MyCheese show full cax-sco chain (ThinCrust→Base→Pizza, Cheese→Topping→Pizza).
Display "Model familiarising with MCP tools — calling help()…" caption during the help() cycle so viewers understand the setup phase. Hold 30s after the model finishes reading the manifest before injecting Turn 0, ensuring chat is truly idle and the first question lands cleanly.
When called with a single iri, expandNode now calls navigateToIri after expanding so the canvas zooms to the node — same behaviour as addNode.
Pre-seed ontology-painter-config localStorage on the Ontosphere origin before the stage iframes load — same pattern as OWUI auth injection. Ensures auto-layout is active for every recording without manual setup.
…ault vocabularies Add ?ontologies= URL param (replace mode) to override the 6 default additionalOntologies with an explicit set. Demo now passes ?ontologies=owl,rdf,rdfs so only the 3 W3C core vocabs land in urn:vg:ontologies — PROV, P-PLAN, QUDT, and spw.ttl no longer pollute the reasoner input and OWL-RL correctly classifies pizza1→SalamiPizza, pizza2→HawaiianPizza, pizza3→MargheritaPizza via cls-svf1 + cax-eqc2. ?ontology= (singular) retains the original add-on-top behaviour. Also: rewrite Socratic turn questions to pure natural language (no explicit tool names), fix PizzaTopping/PizzaBase as independent classes (not subClassOf Pizza) to prevent prp-domain/prp-range from inferring ingredients as pizzas, increase test timeout to 25 min, add explicit page.video().saveAs() to guarantee webm is written under xvfb, use constant refs in MCP tools instead of magic numbers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ite time All quads entering the N3 shared store pass through skolemizeQuad(), which replaces BlankNode subjects/objects with deterministic urn:vg:bnode:<8-char> NamedNode IRIs keyed by graph+localName. This makes blank-node OWL restrictions (and any other blank-node RDF) visible on canvas, navigable, and reachable via MCP tools — without changing N3DataProvider, Reactodia, or MCP schemas. Covers all write paths: importSerialized (Turtle/RDF parsing), syncLoad, syncBatch, renameNamespaceUri, and the OWL-RL reasoner inferred write-back. The dedup exists-check in importSerialized uses the skolemized quad so IDs are consistent. exportGraph de-skolemizes urn:vg:bnode: back to real blank nodes before serializing, so exported Turtle/JSON-LD/RDF-XML is clean RDF. bnodeSessionMap is reset on clear() via resetSharedStore(), so IDs are stable within a session and fresh on reload. Verified by three unit-test variants (named nodes, blank nodes via applyBatch, blank nodes via loadRDFIntoGraph) and a new e2e test that calls loadRdf with inline Turtle and asserts urn:vg:bnode: subjects in getLinks output.
…ization Blank node IRIs are now derived from FNV-1a hashing of each blank node's sorted predicate-object pairs, not from a random ID stored in a session map. Benefits: - Deterministic: same Turtle input → same urn:vg:bnode: IRIs across reloads - No session state: no bnodeSessionMap to reset or leak between loads - Semantically correct: two restrictions with identical structure get the same IRI (they ARE the same restriction) - Nested blank nodes resolved bottom-up so outer hash incorporates inner IRI importSerialized now buffers the full quad stream before skolemizing, so the content-hash is computed over the complete set of parsed triples. syncLoad and syncBatch deserialize to arrays first, then skolemize in one pass. The OWL-RL reasoner inferred write-back is also batched and skolemized.
Worktree test files run under the main workspace vitest, but relative imports in the worktree resolve to worktree src/ while the @ alias inside imported app code resolves to main workspace src/. This creates split store instances — the test mocks the worktree's store but the component reads the main workspace's un-mocked store, causing "rdfManager worker not initialised" and broken assertion failures in 4 tests. Excluding .worktrees/** prevents this cross-branch module bleed.
- Extract shared timing constants (canvasConstants.ts) so addNode pipeline delay, loadRdf propagation wait, and layout debounce are defined once and stay in sync - Tighten T4 blank-node prompt: drop redundant warning (manifest already tells qwen3 addTriple cannot encode blank nodes), clarify as "anonymous existential restriction" so qwen3 copies Turtle verbatim - isBusy skips <details> subtrees so qwen3 chain-of-thought tokens in hidden thinking blocks don't keep the busy signal true after the visible response settles - Add --disable-features=BlockInsecurePrivateNetworkRequests to both Playwright configs so private-network fetch (Ontosphere→docker-dev) is not blocked by Chrome's private network access policy - Add demoLog timing helper for step-by-step CI diagnosis - Extend test timeout 25→45 min; fix video saveAs by closing page first
- Gitignore logs/ directory used for background task output - CLAUDE.md: document the tee-to-logs/ convention so background command output is always findable and tail -f works naturally - Add solution doc for OWUI WebSocket blocked by Playwright
Replaces PO-pair content hash with fnv1a32(blankNode.id). Each addTriple call is a single-quad batch, so the old hash saw only one PO pair and produced a different IRI per call — fragmenting OWL restrictions. Label-only hash: _:b0 always maps to the same urn:vg:bnode: IRI regardless of batch size or call order. For loadRdf / applyBatch, N3.js assigns unique sequential labels per document so distinct restrictions still get distinct IRIs.
… regression test Guard: reject '[...]' passed as subject/object IRI with a clear error message pointing to explicit label syntax instead. Description update: documents that the same blank node label across multiple addTriple calls resolves to the same urn:vg:bnode: IRI, and that each distinct restriction needs a distinct label. Test: adds 4th variant to the OWL restriction collapse regression suite — individual addTriple calls (the MCP path), verifies both blank nodes get distinct skolem IRIs and ind1 is classified correctly.
shorten-idle.py: detects frozen sections via ffmpeg freezedetect at 0.5fps (ignores sub-2s animations), shortens blocks longer than min_freeze_sec to digest_sec, outputs mp4 directly via libx264. collect-demo-videos.mjs: runs shorten-idle after collecting each webm, producing a trimmed mp4 in one step. Falls back to plain ffmpeg conversion if the script fails.
Hardens socratic spec for blank-node pipeline reliability (previous session). Re-recorded with new shorten-idle pipeline: 9.8min raw → 202s trimmed mp4.
video.path() after page.close() returned the transient .playwright-artifacts-0 path which was already moved by the time fs.existsSync() ran. video.saveAs() waits for the final write before resolving. Re-recorded with label-only blank-node skolemization fix in place.
… logging
addTriple guard: IRIs never contain spaces — catch Turtle/Manchester fragments
passed as IRI values (e.g. "ex:hasPart someValuesFrom ex:Foo"). Returns an
error with the correct blank-node pattern so the model can self-correct.
Demo spec: expose __demoLog__ via context.exposeFunction so tool calls from
the Ontosphere iframe are captured in the log file. iframe console events
don't bubble to the page so appFrame.on('console') was silently dropping them.
- loadRdf: validate Turtle snippet before parsing — detect corrupted @Prefix declarations (missing IRI) and bare http:// subjects (missing angle brackets); return descriptive errors with specific fix guidance - loadRdf: auto-inject missing BUILTIN_PREFIXES so callers can omit @Prefix lines for owl:, rdf:, rdfs:, ex:, xsd: - addTriple/addNode: echo the bad IRI value in space-in-IRI error; detect leading-space blank-node pattern (" :r1" → "_:r1") with specific correction hint - Starter prompt (RelaySection, demo spec, pizza-demo-setup, README): add explicit retry instruction — if success:false, fix and retry - Help rule #7: same retry guidance - Demo spec T4: simplify to Socratic question naming owl:equivalentClass, owl:Restriction, owl:someValuesFrom — no verbatim Turtle to copy - Filter urn:vg:bnode: skolemized IRIs from canvas node type display (RdfElementTemplate, NodePropertyEditor) and property editor (rdfPropertyEditor) - N3DataProvider: expose SYNTHETIC_VG_PROPS for template filtering
…p OWUI video - Replace addLink → addTriple in all 4 seed files (156 calls were silently failing — tool was renamed in a prior refactor) - Regenerate all mcp-demo markdown and SVG snapshots with working edges - Re-record all 6 non-OWUI demo videos with correct graph structure - Exclude demo-openwebui-socratic videos (OWUI server currently unavailable) - Skip openwebui-socratic spec in playwright.demo.config.ts until re-recorded
collect-demo-videos.mjs matched result dirs by longest spec name first, preventing pizza-tutorial-chat from overwriting pizza-tutorial video.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
skolemized to stable
urn:vg:bnode:<hash>IRIs; filtered out of canvastype displays and node editors so they never surface to users
addTripleandaddNodereject IRIs containingspaces with targeted hints (detects leading-space blank-node label
corruption from qwen3);
loadRdfvalidates Turtle before parsing(catches missing
@prefixIRI and barehttp://subjects) andauto-injects missing built-in prefixes
RelaySectionall now explicitly tell the model to retry failed callsaddLink→addTriplecalls across seedfiles and specs (tool was renamed in a prior refactor, calls were
silently failing); fixed
collect-demo-videos.mjsprefix-collision bugthat caused
pizza-tutorialto show the chat video; re-recorded all 6non-OWUI demo videos with correct graph edges