Skip to content

Incremental reactivity via @preact/signals-core + architecture decisions (part of #88) - #89

Merged
BorisTyshkevich merged 15 commits into
mainfrom
spike/signals-core
Jun 30, 2026
Merged

Incremental reactivity via @preact/signals-core + architecture decisions (part of #88)#89
BorisTyshkevich merged 15 commits into
mainfrom
spike/signals-core

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

What

Incremental adoption of @preact/signals-core for state reactivity (ADR-0001 / #88), plus the architecture decisions and contributor discipline that came out of the spike.

Code — Part A signal slices (full suite + per-file coverage gate green)

  • resultView + running → signals; the results pane and Run button repaint via effect()s (manual setRunBtn/renderResults removed; run-start writes batch()ed, bookkeeping set before the flip so the synchronous effects read current values).
  • libraryName + libraryDirty → signals; the header title repaints via an effect; the now-unused updateLibraryTitle seam removed.
  • (tabs/activeTabId/sidePanel were already converted.)

Docs / decisions

  • ADR-0001 → Accepted + a Preact schema-panel spike addendum: a component model removes the in-place-mutation pain but buys a second render paradigm the roadmap doesn't justify → stay signals-core, no UI framework. Evidence on spike/preact-schema (not merged).
  • CLAUDE.md rule 5 (no framework; signals for state + imperative-seam adapters for islands; CM6 pre-approved behind an EditorPort seam — Adopt CodeMirror 6 behind the EditorPort seam (replace the hand-rolled textarea editor) #21) and a Working-discipline section (inbox findings; reconcile forward work; friction → memory).
  • CHANGELOG.md [Unreleased], a PR reconcile checkbox, and a "Roadmap item" issue template.

Not here (follow-ups — see the #68 build order)

The schema slice (Phase 1 remainder), CM6 / EditorPort (#21), and the editor-intelligence + graph work.

Part of #88 — the migration continues; this lands the scalar slices + the settled architecture.

🤖 Generated with Claude Code

BorisTyshkevich and others added 13 commits June 29, 2026 23:02
Add src/core/signal.js — a ~70-line signal()/effect()/batch() reactive core
(100% covered) — and convert the tabs slice (state.tabs + state.activeTabId) to
signals end-to-end to evaluate incremental, framework-free reactivity.

- state.js: tabs/activeTabId are signals; activeTab()/tabsForSaved/pruneTabLinks
  read .value (insulating all 16 activeTab() callers from the change).
- tabs.js: selectTab/newTab/loadIntoNewTab/closeTab just mutate the signals;
  refresh() is deleted.
- app.js: one effect() reads the tab signals and repaints the strip + editor +
  results + Save button — the old refresh(), but self-triggering and impossible
  to forget. Result-data repaints still call renderResults directly.
- Test churn: per-file gate stays green (1033 tests). tabs.test.js loses its
  "selectTab triggers repaint" assertions — that responsibility moved to the
  app-level effect — and asserts state transitions instead.

Spike only (branch spike/signals); not wired beyond the tabs slice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQ6VFCwwUWnU3YgHQK2K5k
Second slice, chosen because sidePanel has NO accessor helper (unlike tabs'
activeTab()), so every reader changes — the worst case for churn.

- state.js: sidePanel is a signal.
- saved-history.js: 6 reads → .value; switchTo() sets .value (filter cleared
  first, since the effect runs synchronously on assignment) and drops its manual
  renderSavedHistory() call.
- app.js: an effect() repaints the side panel on sidePanel change; the
  history-record bridge read → .value.
- Tests: saved-history.test.js (19), state.test.js (2), app.test.js (1) — all
  mechanical .value. Gate green (1033 tests).

Finding: with no helper, churn is purely mechanical and concentrated in the
panel's own test file; no behavioral test rewrites were needed (unlike tabs,
where the repaint responsibility relocated). Confirms the pattern generalizes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQ6VFCwwUWnU3YgHQK2K5k
Records the reactivity decision, the measured option comparison
(in-house signals ~0.45KB vs Preact ~7.3KB vs Solid/React), the two-slice
spike evidence, and the per-slice + accessor-helper migration rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQ6VFCwwUWnU3YgHQK2K5k
Drop-in swap on top of the two converted slices: @preact/signals-core has the
same .value / signal / effect / batch API, so the only changes are import lines
+ deleting src/core/signal.js and its test. Suite green (1022 tests; -11 from
removing the primitive's own tests). Adds @preact/signals-core ^1.14.3.

Trade vs hand-rolled: +1.4 KB gzip artifact (~1%) and one deliberate dependency,
in exchange for a battle-tested, glitch-free core with computed()/untracked() and
no ~70 lines + 178 test lines of reactive code to own.

NOTE if adopted: add @preact/signals-core to THIRD-PARTY-NOTICES.md and update
ADR-0001 to record signals-core as the chosen primitive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQ6VFCwwUWnU3YgHQK2K5k
Update ADR-0001 (decision + measured 3-way comparison: signals-core +1.4KB vs
hand-rolled +0.45KB vs Preact +7.3KB), add the @preact/signals-core MIT notice
to THIRD-PARTY-NOTICES.md (two→three inlined deps), and fix the build.mjs
dependency comments. CLAUDE.md rule 4 ('two deps') to be updated on adoption.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQ6VFCwwUWnU3YgHQK2K5k
resultView/running become signals; the results pane and Run button now
repaint via effects in createApp instead of manual setRunBtn/renderResults
calls in the run flow. The tab effect drops renderResults — a new results
effect reads activeTabId + resultView + running (and, via activeTab(),
tabs). Run-start writes are batched, and the run bookkeeping (runT0,
elapsed_ns) is set *before* the run signals flip, since the effects fire
synchronously and read it. The format-error path keeps its explicit
renderResults (an in-place tab.result write with no signal change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLVYgQpkbEbHKwEAA4aawZ
…o signals

The header title (name + unsaved-changes dot) repaints via a libraryName/
libraryDirty effect in createApp instead of manual updateLibraryTitle/
renderLibraryTitle calls scattered across saved-history.js, file-menu.js, and
the save popover. Removed the now-obsolete app.updateLibraryTitle seam.

The editingLibrary-driven renders stay explicit (editingLibrary is not a
signal); finish() now leaves edit mode before renameLibrary so the title
effect repaints the button view rather than a transient input. The
data-driven calls (saveJsonAction, afterLibraryChange, favorite/delete/rename)
are dropped — equality-gated signals only skip a repaint when the title is
already correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLVYgQpkbEbHKwEAA4aawZ
…ADR-0001 Accepted

Bump CLAUDE.md rule 4 from two to three bundled runtime deps (adds
@preact/signals-core, pointing at ADR-0001). Move ADR-0001 to Accepted and
reconcile its "behind accessor helpers" wording with the validated helper-free
`.value` reality: the accessor helper is now a guideline (for slices with many
scattered readers), not a rule — the sidePanel, resultView/running, and
libraryName/libraryDirty slices all converted fine without one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLVYgQpkbEbHKwEAA4aawZ
…dapters

Record the architecture decision from #88 + the Preact spike (ADR-0001): state
reactivity via @preact/signals-core, no React/Preact/Solid, and the hard
third-party/high-frequency-pointer surfaces (editor, graphs, Chart, result grid)
stay imperative behind injected seams. CodeMirror 6 pre-approved as the next dep
behind an EditorPort seam (#21, enabling #84). Shared UI primitives are extracted
on a second consumer, not built speculatively.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLVYgQpkbEbHKwEAA4aawZ
Record the spike outcome in ADR-0001: the component model removes the schema
panel's in-place-mutation anti-pattern and hits the 100% coverage gate, but at
+6.8 KB gzip (measured) and a second render paradigm + icon/h/columns
integration seams paid app-wide for one panel. Recommendation: do not adopt
Preact now — stay signals-core; keep the schema panel a documented imperative
exception (or convert it with replaced Set/Map signals). Revisit only when
several complex rich-local-state panels are actually on the roadmap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLVYgQpkbEbHKwEAA4aawZ
…ep count

Add a Working-discipline section — surface out-of-scope findings as `inbox`
issues; reconcile forward work (roadmap #68, the issue's Goal/Acceptance, the
ADR addendum, CHANGELOG [Unreleased], close obsoleted via Closes #N) in the same
commit; convert friction into memory. Also reconcile the intro line with rule 4
(two → three bundled runtime deps, now that @preact/signals-core is in).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLVYgQpkbEbHKwEAA4aawZ
…eleased]

Per the new working-discipline rule: record the @preact/signals-core adoption
(ADR-0001 / #88) and the rejected Preact spike as an [Unreleased] entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLVYgQpkbEbHKwEAA4aawZ
Implement the forward-work-tracking discipline: a PR checklist item to reconcile
affected tracked work (roadmap #68 / issue body / ADR / CHANGELOG), and a
"Roadmap item" issue template (Goal / scope / key implementation / acceptance /
re-evaluation trigger / tracking) for structured roadmap issues.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLVYgQpkbEbHKwEAA4aawZ
@BorisTyshkevich BorisTyshkevich mentioned this pull request Jun 30, 2026
93 tasks
BorisTyshkevich and others added 2 commits June 30, 2026 13:33
…ditor harness

tests/e2e/editor.html loads /src as raw ESM (no bundler), but signals adoption
added a bare `import { signal } from '@preact/signals-core'` to src/state.js
that the browser can't resolve → state.js fails to load → the harness never sets
window.__ready → every editor-insert / editor-alignment spec times out on
page.waitForFunction. Add an import map pointing the bare specifier at the local
ESM build (the e2e server serves the repo root, so /node_modules is live),
mirroring how pipeline.html imports dagre. Unit tests + the bundle were
unaffected (node resolves bare specifiers; esbuild inlines them).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLVYgQpkbEbHKwEAA4aawZ
# Conflicts:
#	CHANGELOG.md
@BorisTyshkevich
BorisTyshkevich merged commit b6c9923 into main Jun 30, 2026
6 checks passed
@BorisTyshkevich
BorisTyshkevich deleted the spike/signals-core branch June 30, 2026 11:44
BorisTyshkevich added a commit that referenced this pull request Jul 19, 2026
…idelity + admonitions

Owner decision at the PR gate, amending #315's original no-Markdown-dependency
non-goal: the hand-written block/inline parser is replaced by marked@18.0.6
used strictly as a PURE LEXER (string -> tokens; marked.parse/innerHTML are
never called — the sixth bundled runtime dependency, measured +44,130 bytes
raw / ~3.0% on dist/sql.html). parseDocMarkdown's public contract, all five
limits, the https-only link policy (reference links now resolve through it),
the never-throws fallback, and the fixed-seed fuzz pass all survive; the
mapper stays fail-closed (images/raw HTML/rejected links -> visible literal
text).

DocInline is now a RECURSIVE tree mirroring marked's inline tokens 1:1 —
**see the [guide](/docs/x)** keeps its link inside the bold, link text can
carry code, and del renders — with the nesting budget guarding inline
recursion too. New 'admonition' DocBlock: Docusaurus :::tip/note/info/
warning/danger/important containers (fence-aware pre-scan, unterminated
openers stay literal) render as styled asides — the visible wart on real
ClickHouse doc bodies. CommonMark upgrades: setext headings, loose lists,
reference links. markdown-lite reverts to its simpler pre-lift shape (panels
profile unchanged, tests unmodified); the e2e editor.html import map gains
marked (the #89 unbundled-harness rule).

Part of #60.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj
BorisTyshkevich added a commit that referenced this pull request Jul 20, 2026
…n-modal pane, structured sources, system.documentation Markdown (#320)

* feat(#313): pure doc contracts + system.functions capability/query/normalize core

src/core/doc-types.ts: shared DocKind/DocTarget/DocLookup/DocSummary/DocEntry
contracts (types-only). src/core/doc-capability.ts: capability decision from
system.columns (name required, every rich column optional and independent —
no serverVersion gate), dynamic SELECT construction over confirmed columns
with case-insensitive exact/lower/upper name matching, and raw-row
normalization (leading-blank-line summaries, String categories -> string[],
Nullable(UInt8) tri-state). 100% covered.

Part of #60 (Phase 1, #313).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* feat(#313): target-aware docSummary/docEntry with connection-generation safety

src/net/ch-client.ts: silent system.functions capability probe
(loadFunctionsDocColumns via system.columns) + prebuilt-SELECT row fetch
(loadFunctionDocRow), both on the tryQueryData seam.
src/application/schema-catalog-service.ts: lazy once-per-connection deduped
capability probe (null = transient, retried next batch, never a storm;
[] = durably unavailable), kind:name entry cache with concurrent-lookup
dedup, kind-mismatch dual-key caching (fetched row is the truth), and a
docGeneration counter bumped by both invalidate() and loadReferenceImpl so
in-flight lookups across a reconnect resolve 'unavailable' without cache
writes. docSummary projects docEntry's cache — one fetch serves both.
entityDoc() unchanged (deleted when CM6 migrates in a later commit).

Part of #60 (Phase 1, #313).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* refactor(#313): extract non-modal drawer chrome primitive from results.ts

src/ui/drawer.ts: buildDrawerChrome (panel/head/title/close, class-prefixed,
no backdrop/focus-trap baked in) + attachDrawerResize moved verbatim behind a
narrow DrawerResizeApp seam. openCellDetail/openRowsViewer compose it with
their local backdrop/stacking unchanged — results tests pass unmodified
(117/117). The 'shared Drawer primitive deferred to #60' from the results.ts
comment; third consumer (docs pane) lands next.

Part of #60 (Phase 1, #313).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* refactor(#313): extract chLanguageExtension into src/editor/ch-lang.ts

CM6 ClickHouse dialect construction now takes AssembledReference | null
directly instead of the app controller, so the docs pane (SQL example
highlighting, phase-3 Markdown code blocks) can reuse it without importing
the editor adapter. langExtensionFor stays as a thin delegate; lazy first-
mount resolve and rebuild-on-refData-arrival unchanged.

Part of #60 (Phase 1, #313).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* feat(#313): non-modal documentation pane + openDocEntry action

src/ui/doc-pane.ts: persistent right-side pane on the drawer chrome (own
.docs-* classes — invisible to the modal .cd-* stacking), single instance
with content replacement, loading/found/missing/unavailable+Retry states,
structured entry rendering (kind/since badges, cycle-guarded alias
navigation, categories, deterministic/higher-order, examples via the
injected CodeViewer seam with ClickHouse highlighting + exact-text Copy),
capture-phase Escape with preventDefault so the global cancel-query
shortcut never double-fires, focus restore, stale-response discard.
docPanePx resize state end-to-end (state/prefs/splitters 'docPane' axis;
attachDrawerResize gains an optional stateKey/axis param, existing callers
unchanged). CodeViewerOptions gains optional languageExtension override.
Pane closes on sign-out alongside catalog.invalidate().

Part of #60 (Phase 1, #313).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* feat(#313): CM6 hover/completion docs on docSummary, F1 command, pure classifier; drop entityDoc

src/core/doc-context.ts: pure phase-1 target resolver (word detection +
exact/lower/upper resolution over refData functions; literal suppression is
the caller's contract). codemirror-adapter.ts: one shared
renderFunctionSummary powers hover and completion info (refData lookup stays
the existence gate — no SQL for unknown identifiers), compact summary with
since/alias badges and an accessible Open reference button into the docs
pane, isConnected liveness guards after every await, and an editor-local F1
keymap command (handled -> true, no target -> false so the browser default
survives). shortcuts dialog documents F1. Dead seam deleted end-to-end:
entityDoc/docCache (service), loadEntityDoc (ch-client), wiring + fake-app
stubs. Adds an explicit no-SQL-on-keystroke enforcement test.

Part of #60 (Phase 1, #313).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* refactor(#313): editor stays a leaf — inject openDocEntry, enforce editor↛ui boundary

Review finding: codemirror-adapter.ts imported ui/doc-pane.js, reversing the
editor-as-leaf layering the rest of the codebase relies on. The adapter now
declares openDocEntry?: (target: DocTarget) => void on its injected app
surface; app.ts binds it to ui/doc-pane's openDocEntry(app, target) at wiring
time (App type gains the member). toDocPaneApp and the adapter's borrowed
document/prefs/CodeViewer fields are gone. build/check-boundaries.mjs gains
the src/editor -> src/ui rule with an "except" mechanism for the two
deliberate carve-outs predating it (dnd-mime constants, the dom hyperscript
helper) so the direction is enforced, and the e2e editor.html harness
mirrors the app.ts wiring.

Part of #60 (Phase 1, #313).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* test(#313): real-browser e2e spec for F1, Open reference, and the non-modal docs pane

tests/e2e/editor-docs.spec.js: F1 opens the labelled complementary pane with
the version-exact entry; unhandled F1 is left to the browser default; the
pane is non-modal (no backdrop, editor keeps accepting input); Escape inside
the pane closes it without leaking to bubble-phase document listeners and
restores editor focus; the completion-info Open reference button is a real
keyboard-activatable element opening the same pane; unknown entries render
the missing state. editor.html harness serves one canned rich entry (sum)
via window.__docFixtures, missing otherwise. Runs in CI (Playwright is
CI-only in this environment).

Part of #60 (Phase 1, #313).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* feat(#314): structured doc sources — formats, table/database engines, data types

src/core: DocKind gains format/table-engine/database-engine/data-type;
DocEntry gains optional syntaxFull/related/facts. Per-source capability from
system.columns (name required, everything else independent — no version
gate), SELECT builders over confirmed columns only (system.formats never
probes/selects its nonexistent syntax column — asserted in tests), and
per-source normalizers (format boolean columns -> human-readable facts,
related as same-kind targets or label chips from String or Array shapes,
data-type alias_to, canonical casing preserved).
src/net/ch-client.ts: generalized loadDocTableColumns (fixed internal table
allowlist, never caller-interpolated) + loadDocRow; Phase 1 loaders stay as
thin wrappers. schema-catalog-service routes docEntry by kind with fully
independent per-source capability probe/cache/retry state, reset together
on invalidate/reconnect; function-kind behavior untouched.

Part of #60 (Phase 2, #314).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* feat(#314): strong-context SQL classifier + CM6 F1/completion routing

src/core/doc-context.ts: resolveDocTarget gains ranked strong contexts —
top-level FORMAT clause in either FORMAT/SETTINGS order (never confusing
format()/formatDateTime(), adjacency-based), ENGINE = for table vs database
DDL, and strong data-type positions (CREATE column defs, CAST(x AS T),
x::T, {p:T} via the existing param-scan) with innermost nested-type
resolution; statement-scoped via sql-lex tokens, pure, no SQL. Optional
DocContextOptions.formats validates FORMAT names against refData.
codemirror-adapter.ts: F1 uses the extended resolver; format completion
info renders the shared summary card with Open reference; function hover
unchanged; no metadata query on ordinary typing (enforcement test extended).

Part of #60 (Phase 2, #314).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* feat(#314): schema-surface doc actions + pane rendering for the new kinds

src/ui/doc-pane.ts: kind labels for format/table-engine/database-engine/
data-type, syntaxFull via the injected CodeViewer, facts chips, related
items as in-place navigation buttons (label-only stay inert chips), and a
bounded (20) session-local back stack unified with alias navigation —
rendered in every state, cleared on fresh open/close/connection change.
schema-catalog-service: sync docKindAvailable(kind) hint (reads capability
state, never probes). schema-detail gains an engine row (below the head —
the no-button-in-head contract holds) with Open engine reference; column
type cells in the tree (schema.ts) and detail table gain Open type
reference targeting the OUTERMOST type family (pure outerTypeName in
core/type-display — no caret to resolve innermost from); actions are real
labelled buttons, hidden when the source is durably unavailable, existing
click/dblclick/Shift-click/drag gestures untouched. loadTableDetail now
returns system.tables.engine.

Part of #60 (Phase 2, #314).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* fix(#314): phase-2 review fixes — comment-headed DDL classification, styling, e2e

Review finding (confirmed): a leading -- or /* */ comment before CREATE/
ATTACH DDL broke classification — isDatabaseDDL misread the statement head
(database engines resolved as table engines) and inColumnListTypeRegion
missed column types entirely. Statement head now skips comment tokens
(stmtHeadIdx, mirroring from-scope's convention) with regression tests.
Adds the missing CSS for every phase-2 interactive element (.docs-back,
.docs-related-link, .docs-facts-list, .docs-syntax-code mirroring
.docs-example-code, .schema-type-doc/.schema-engine-doc, engine row) incl.
the :focus-visible outline convention across all docs-pane buttons.
Extends tests/e2e/editor-docs.spec.js: F1 on FORMAT/ENGINE/column-type
contexts, related navigation with Back (kind-keyed harness fixtures).
Corrects the dual-key comment in schema-catalog-service (case-mismatched
structured lookups DO dual-cache — now tested) and the stale doc-context
module contract paragraph.

Part of #60 (Phase 2, #314).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* feat(#315): version-gated system.documentation capability, loader, kind mapping

src/core/format.ts: parseServerVersion/versionAtLeast generalize the
supportsExplainPretty one-off (behavior unchanged, tests unmodified).
src/core/doc-documentation.ts (pure): documentationProbePolicy — parsed
< 26.6 skips (durably unavailable, ZERO queries or probes), >= 26.6 or
unparsable version probes once; capability requires name/type/description
with source OPTIONAL (agreed amendment — live 26.6.1 servers expose only
three columns); 15 known Enum8 labels map to stable DocKinds plus
forward-compatible Codec/Metric/System Table, unknown labels stay readable
as kind 'unknown' preserving the server label; by-(type,name) and name-only
disambiguation SELECTs (LIMIT 20); normalizer emits MarkdownDocEntry (raw
markdown body, 1MB byte-bound truncation + oversized flag).
schema-catalog-service: version-policy-gated documentation capability on
the shared generation machinery; source preference — structured found/
missing wins, durable structured unavailable falls through, no-loader kinds
(settings etc.) go straight to documentation; new docMarkdown(target) and
docDisambiguate(name) APIs; sourceTable records which source supplied an
entry. DocKind widens with 12 broad kinds + 'unknown'.

Part of #60 (Phase 3, #315).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* feat(#315): bounded pure Markdown-subset parser reusing the markdown-lite inline layer

src/core/doc-markdown.ts: block-level parser (ATX headings, paragraphs,
bounded nested lists with ordered start numbers, fenced code preserved
exactly, single/bounded block quotes, thematic breaks, simple tables with
malformed-line paragraph fallback) over markdown-lite's lifted parseInline
(now exported with an injected linkPolicy — no second inline parser;
parseMarkdown/panels.ts behavior unchanged, tests unmodified). Raw HTML,
images, setext headings, and reference links stay literal visible text.
https-only default link policy + pure allowlisted clickhouseDocUrl relative
mapper (scheme/host/.. rejected -> plain text). Exported limits from #315
(1MB input, 20k nodes, depth 16, 250KB code blocks, 1k links) each
truncate/flatten with flags; parseDocMarkdown never throws — total
fallback to one literal text block, hostile linkPolicy/toString covered.
Fixed-seed fuzz pass (300 iters + pathological inputs).

Part of #60 (Phase 3, #315).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* feat(#315): safe AST-to-DOM markdown view + pane markdown-subset rendering

src/ui/doc-markdown-view.ts: strict DOM-construction renderer (no
innerHTML) over the doc-markdown AST — headings offset below the pane
title, lists/quotes/tables/hr, sql-tagged fences through the injected
CodeViewer with ClickHouse highlighting (other fences plain pre/code),
per-block exact-text Copy buttons, parser-approved links only rendered as
target=_blank rel='noopener noreferrer' anchors, quiet truncation notes.
doc-pane renders markdown-subset entries: kind badges for every #315 kind
(unknown shows the preserved serverTypeLabel), muted source path, oversized
note, and a visually-secondary 'View latest on clickhouse.com' link ONLY
when latestDocUrlFromSource can safely derive it from a docs/**.md source
path via the existing traversal/scheme-rejecting mapper — otherwise
omitted. Markdown code viewers register into the pane's existing viewer
teardown; structured rendering path unchanged.

Part of #60 (Phase 3, #315).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* feat(#315): broad classifier contexts + accessible disambiguation; F1 name-only fallback

src/core/doc-context.ts: six new strong positional contexts ranked with the
existing ones — query-level SETTINGS names ('setting'; 'mergetree-setting'
when CREATE/ATTACH TABLE DDL carries a preceding ENGINE = *MergeTree*
clause), FROM/INSERT INTO FUNCTION table-function calls, CODEC(...) list
elements, INDEX ... TYPE skipping-index types, and system.<name> after
FROM/JOIN. Aggregate combinators deliberately stay unclassified (name-shape
guess, not a caret position — reachable via disambiguation instead).
doc-pane: openDocDisambiguation — 0 matches -> missing, 1 -> straight to
the entry, 2+ -> an accessible labelled list of kind/name/summary buttons
navigating in place, integrated with the back stack (Back returns to the
list). F1 contract extended per the spec: an unresolved bare word now opens
disambiguation through the new injected app.openDocDisambiguation seam
(editor still never imports UI); no-word/literal positions still return
false. Harness + e2e spec extended.

Part of #60 (Phase 3, #315).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* fix(#315): version-gate 'skip' verdict is per-call, not durable (reconnect race)

Review finding (confirmed): ensureDocumentationCapability read the current
state.serverVersion and durably cached a pre-26.6 'skip' as an unavailable
capability — but loadVersion()'s round-trip is not sequenced with
resetDocsState(), so a doc lookup racing a reconnect could read the OLD
connection's version and lock system.documentation off for the entire new
session. The skip verdict now returns per-call without touching the cached
capability: still zero network for pre-26.6 servers, self-heals as soon as
the version catches up; probe RESULTS stay durable. Regression test
simulates the stale-version window then the version update.

Part of #60 (Phase 3, #315).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* fix(#60): live-verification polish — prose summaries and markdown-rendered structured fields

Verified against a real ClickHouse 26.6.1 server: doc text in
system.functions/data_type_families/documentation descriptions carries
Markdown (Docusaurus :::tip admonitions, links, tables). Summaries (hover
cards, disambiguation rows) now derive from the first PROSE line — shared
firstProseLine skips blank/admonition/table-row/fence/thematic-break lines
and strips ATX heading markers (doc-documentation's local copy replaced).
The pane's structured long-text fields (description, arguments, parameters,
returned value) render through the existing safe markdown-subset renderer
with the same link policy, SQL-fence highlighting, Copy, and viewer
teardown as documentation entries; summary/signature/examples unchanged.

Part of #60.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* feat(#60): adopt marked@18 as the Markdown lexer — recursive inline fidelity + admonitions

Owner decision at the PR gate, amending #315's original no-Markdown-dependency
non-goal: the hand-written block/inline parser is replaced by marked@18.0.6
used strictly as a PURE LEXER (string -> tokens; marked.parse/innerHTML are
never called — the sixth bundled runtime dependency, measured +44,130 bytes
raw / ~3.0% on dist/sql.html). parseDocMarkdown's public contract, all five
limits, the https-only link policy (reference links now resolve through it),
the never-throws fallback, and the fixed-seed fuzz pass all survive; the
mapper stays fail-closed (images/raw HTML/rejected links -> visible literal
text).

DocInline is now a RECURSIVE tree mirroring marked's inline tokens 1:1 —
**see the [guide](/docs/x)** keeps its link inside the bold, link text can
carry code, and del renders — with the nesting budget guarding inline
recursion too. New 'admonition' DocBlock: Docusaurus :::tip/note/info/
warning/danger/important containers (fence-aware pre-scan, unterminated
openers stay literal) render as styled asides — the visible wart on real
ClickHouse doc bodies. CommonMark upgrades: setext headings, loose lists,
reference links. markdown-lite reverts to its simpler pre-lift shape (panels
profile unchanged, tests unmodified); the e2e editor.html import map gains
marked (the #89 unbundled-harness rule).

Part of #60.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* docs(#60): reconcile marked adoption — CLAUDE.md rule 4 (six deps), CHANGELOG

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* fix(#60): marked-swap review fixes — CommonMark fence-length tracking, markdown-lite image guard

Review findings (both confirmed live against marked's real lexer):
(1) the admonition pre-scan tracked fences as a boolean toggle, so a
shorter inner run desynced it from marked's CommonMark rule (closing fence
= same char, run length >= opener, bare) and tore content across
admonition boundaries — fence state now records char+length via a shared
nextFenceState helper, with mismatched-length and suffixed-run regression
tests; (2) the markdown-lite revert dropped the image lookbehind, turning
![alt](url) into a real link in dashboard Text panels — restored with an
inert-literal regression test.

Part of #60.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* fix(#60): review feedback — reference link, global Escape, markdown examples

Three live-usage findings from the owner's review of the deployed build:

1. The hover/completion card's "Open reference" button becomes a
"(reference — F1)" affordance on the badges row (after the since badge):
the single word "reference" is a standard blue link (--accent), still
keyboard-activatable; shared openReferenceLink helper serves both the
function and structured summary cards, with the previously-missing
hover-meta/badges/link CSS added.

2. Escape now closes the reference pane from ANYWHERE — not only with
focus inside it. shortcuts.ts's handleKeydown gains a layered Escape
branch (close-doc-pane BEFORE cancel-running-query; CM6-consumed Escapes
still win via the defaultPrevented guard), driven by an injected
app.closeDocPane hook (new isDocPaneOpen export; editor stays a leaf).
The pane's own focus-inside capture handler is unchanged. e2e harness
mirrors the wiring; new from-anywhere e2e case.

3. The examples field is a MARKDOWN document on real servers (**bold**
section titles between ```sql/```response fences), not one SQL snippet —
it now renders through the same markdown path as the other long-text
fields: each fence its own block with an exact-text Copy button, sql
fences highlighted via the injected viewer, response fences as plain
preformatted text.

Part of #60.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* fix(#60): remove per-block Copy buttons from markdown code blocks (owner decision)

The Copy button under every rendered fence read as noise; code text
selects/copies normally. Dropped from doc-markdown-view (onCopy option
gone), the now-dead copyExample/clipboard seam and toast wiring removed
from doc-pane, CSS + focus-visible entries cleaned, tests updated.

Part of #60.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* test(#60): fix e2e failures — realistic fenced examples fixture, completion-first Escape layering

Two deterministic CI failures in editor-docs.spec.js (all three browsers),
both harness/spec bugs, production code unchanged: (1) the sum fixture's
examples was a bare SQL line — under the markdown examples rendering that
is a paragraph, so no viewer/pre existed; the fixture now mirrors real
system.functions shape (bold title + fenced sql). (2) the Escape-from-
anywhere test pressed one Escape while typing had left the completion
popup open — CM6's own Escape correctly closes the popup FIRST (the
designed completion -> pane -> query layering, same as observed live);
the test now dismisses the popup before asserting the pane close.

Part of #60.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* test(#60): deterministic Escape-from-anywhere e2e — programmatic doc set, no completion race

The isVisible() completion guard raced CM6's ~100ms popup materialization
(Playwright reaches the check before the popup exists; the popup then
swallows the Escape — the designed completion-first layering). The test now
sets the document programmatically (CM6 only auto-opens completion on
user-typed input), so no popup ever exists and the single Escape is
deterministically the pane's. Sequence verified against the real harness
via build/e2e-serve.mjs in Chromium.

Part of #60.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ytb297gnTS4tQKAiFZArj

* fix(#60): close MAX_DOC_AST_NODES bypass through list items and table rows/cells

mapList/mapTable mapped every item/row/cell unconditionally, so once the
shared node budget was exhausted a bounded 1 MB body of ~250k short list
items or table rows still produced that many empty AST entries — and
doc-markdown-view.ts renders one element per entry, freezing the Workbench.
Each list item, header cell, row, and cell now counts against the budget
and enumeration stops (truncated already set by countNode) once it is
spent. Regression tests: oversized list, oversized table, and a
small-list/table intactness check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019MVwBAjfFthAyPd1eQczhR

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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