Skip to content

feat: blank-node skolemization, MCP hardening, demo fixes#11

Merged
ThHanke merged 68 commits into
mainfrom
feat/blank-node-skolemization
May 8, 2026
Merged

feat: blank-node skolemization, MCP hardening, demo fixes#11
ThHanke merged 68 commits into
mainfrom
feat/blank-node-skolemization

Conversation

@ThHanke
Copy link
Copy Markdown
Owner

@ThHanke ThHanke commented May 8, 2026

Summary

  • Blank-node skolemization — blank nodes written to the RDF store are
    skolemized to stable urn:vg:bnode:<hash> IRIs; filtered out of canvas
    type displays and node editors so they never surface to users
  • MCP tool hardeningaddTriple and addNode reject IRIs containing
    spaces with targeted hints (detects leading-space blank-node label
    corruption from qwen3); loadRdf validates Turtle before parsing
    (catches missing @prefix IRI and bare http:// subjects) and
    auto-injects missing built-in prefixes
  • Relay retry instruction — starter prompt, help text, and
    RelaySection all now explicitly tell the model to retry failed calls
  • Demo fixes — replaced 200+ addLinkaddTriple calls across seed
    files and specs (tool was renamed in a prior refactor, calls were
    silently failing); fixed collect-demo-videos.mjs prefix-collision bug
    that caused pizza-tutorial to show the chat video; re-recorded all 6
    non-OWUI demo videos with correct graph edges

Thomas Hanke 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)
…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
Thomas Hanke and others added 29 commits May 8, 2026 16:44
…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.
@ThHanke ThHanke merged commit 639e6f0 into main May 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant