All notable changes to dikw-web are documented in this file.
The format is based on Keep a Changelog.
Version numbers are standard three-digit SemVer (MAJOR.MINOR.PATCH); package.json.version
is the single source of truth. See [0.0.2] below for why the four-digit VERSION
file format introduced in [0.0.1.0] was dropped.
- dikw-core verification pin single-sourced in
scripts/live-core/harness.mjs.live-integration.ymlno longer sets aDIKW_CORE_VERSIONenv — CI/nightly falls back to the harness'sDEFAULT_CORE_VERSION, the same value local runs use — andbump-dikw-core.ymlnow edits (and reads the current pin from) only the harness file. This unjams the auto-bump loop discovered while bumping to core 0.6.5 (#150): a bump commit that edited a workflow file was doubly blocked — GitHub rejects such a push from a PAT without the Workflows permission, and thegate-integrityrequired check flags any.github/workflows/**edit without a maintainer'sgate-changelabel. With the pin out of the workflow file, future auto-bump PRs need neither. TheDIKW_CORE_VERSIONenv override still works everywhere;docs/integration-verification.mdno longer hardcodes the pin value (one less thing to drift per bump).
- Automated watch-CI + fix (
dikw-web-watch-ciskill) + loop observability (scripts/loop-log.mjs). Turns delivery-loop step 8 from prose into a bounded, self-logging loop: watchgh pr checks --watch, route a real failure to the fresh-contextfixer, rerun the one known flake (graph.spec.tsPixi) at most once, always pull the review prose before merge, and merge explicitly (never--auto, which outraces the review) — gated by MAX_ROUNDS = 3 and a same-failure circuit breaker. Each transition is appended as a structured JSON line to a gitignored.loop-log.jsonl(node scripts/loop-log.mjs <event> [detail]) so an autonomous/background run is diagnosable after the fact (runaway / stuck / flake / silent death). Wires intodikw-web-delivery-workflowstep 9 andCLAUDE.md. Seedocs/adr/0005-delivery-loop-hardening.md.
dikw-web-verify-frontendgains a measured perf + a11y pass (Step 2.5). The frontend-verify skill previously eyeballed thedocs/ui-checklist.mda11y / contrast / perf items. It now uses the already-installedchrome-devtools-mcp:lighthouse_auditfor accessibility + best-practices (the tool excludes performance), plus aperformance_start_trace/stop_tracefor Web Vitals, scored to a rubric — a11y ≥ 0.9 with no new violation, CLS ≤ 0.1 (cross-checking theperf.spec.tsgate), LCP recorded as a soft budget — turning the qualitative items into numbers. Locally-measured, not a new CI gate (Lighthouse + trace timing is runner-dependent). The#graphPixi route audits a11y normally but skips the background-tab perf trace. Seedocs/adr/0005-delivery-loop-hardening.md.
- Reward-hacking gate (
gate-integrityCI job /npm run check:gate). A deterministic check the in-loop agent cannot fool, hardening the delivery loop against an agent weakening the verification to make a check go green.scripts/check-gate-integrity.mjsdiffs the branch against its merge base and fails the PR if a coverage threshold was lowered, the coverageexcludelist grew, a bundle budget was raised, e2eretrieswere raised, a test was deleted/disabled or stripped of assertions, or the gate/CI machinery itself (the script,.github/workflows/**,fixer.md's forbidden list) was edited. The good direction (raising a threshold, adding tests) is always allowed; a deliberate weakening is allowed only when a maintainer adds the visiblegate-changePR label. Until now "don't weaken the tests" was prose only inCLAUDE.md/docs/review-rubric.md/fixer.md— the weakest defense. The job is PR-scoped and a required status check. Seedocs/adr/0005-delivery-loop-hardening.md.
- Flaky graph e2e (
graph.spec.ts > renders a nonblank Pixi graph canvas) — root-caused, not retried away. In React StrictMode's dev double-invoke, the graph mounts twocreatePixiGraphEngine()inits concurrently. The canvas was attached viamount.replaceChildren(app.canvas)inside the async factory, outside the effect'sactiveguard, so a slower-resolving stale init could clobber the live engine's canvas and then empty the mount when itsdestroy()fired — leavingdata-ready="true"over a canvas-less stage.GraphCanvasnow attaches the canvas only inside the active guard (the engine exposes acanvasgetter), so the discarded init never touches the DOM. Production rendering is unchanged (no StrictMode double-invoke there → single attach). The spec also gates ondata-render-count >= 1before reading the canvas contract. - Flaky Mermaid disclosure clicks (
wiki.spec.ts,theme.spec.ts). A baregetByText("flowchart")also matched the<style>Mermaid injects (its CSS mentionsflowchart), so once any diagram had rendered the locator resolved to two nodes and.click()threw a Playwright strict-mode violation. Both specs now scope to the<summary>element (locator("summary", { hasText: … })).
- Chat agent tool calls fail (
fetch failed) in a proxied dev/live setup. dikw-core has no CORS, so the browser keepsserverUrlat the default and routes its/v1reads through the same-origin Vite proxy (VITE_DIKW_PROXY_TARGET→ the real, dynamically-ported core). But the sidecar's/agentcore calls run server-side and bypassed that proxy — they dialed the default port the browser sent (http://127.0.0.1:8765), where nothing listens, so every agent tool (retrieve_knowledge,dikw_health, …) returnedfetch failedand the chat could not read the knowledge base. The sidecar now mirrors the Vite/v1proxy:agentSidecarPlugininjects the Vite-resolvedVITE_DIKW_PROXY_TARGET(honoring.env.local, not just a shell export, and warning on a malformed value) andapplyDevProxyTarget(server/agent/http.ts) routes the default core URL to it. Dev-only by construction — the standalone production sidecar injects no target, so it always dials the configuredServer URLverbatim; a custom, directly-reachableserverUrlis also left untouched. Unit guards:server/agent/http.test.ts"applyDevProxyTarget…" + the injected-target handler test;server/agent/vitePlugin.test.ts"resolveDevProxyTarget…".
live:verifyagent↔core check now reproduces the real browser path.scripts/live-core/verify-agent.mjspreviously POSTed/agentwith the live core's real dynamic URL, bypassing the proxy the browser depends on — so it passed even while the actual chat was broken. It now sends the default core URL (like the browser) and asserts a core tool reachedstatus:"succeeded"(not merely that one was invoked — afetch failedcall still emits a tool_event). This catches the bug above: it fails against a sidecar that can't reach core and passes once the proxy is mirrored.
-
Hero metric value is the documented 27px, not a drifted 30px. A token-migration override silently pinned
.metric-card__valueto 30px over the 27px base rule, contradicting DESIGN.md §3 (which documents the hero-number as 27px twice). Dropped the override so the documented size ships. -
Three sans-uppercase "eyebrows" now use the mono label voice (Mono-Only-Uppercase).
.wiki-info-grid dt(every sibling field label was already mono),.wiki-preview__header, and MB-Web's.mb-labbilingual column header (the twin of the workbench's mono.bi-colhead span) were sans uppercase — the exact AI "eyebrow" tell the type system forbids. Routed onto the mono label role at the shared 0.04em tracking. -
Three mono labels declared a
font-weight: 600no-op. IBM Plex Mono ships only 400/500, so600silently rendered as 500 and diverged from the canonical label weight:.bi-colhead span,.import-step__marker,.trace-invocation__durnow declare 500 (no visual change — they already rendered at 500). -
User Markdown table headers render verbatim.
.markdown-table-wrap thno longer force-uppercases the author's header row (a content-fidelity breach — a header likepH/mRNAwas mangled — and a sans-eyebrow tell). The row stays distinct by weight + surface, not case. The label role's "table heads" remain the workbench's own data-table column labels (mono uppercase); DESIGN.md §3 now spells out the distinction. -
.wisdom-dialog__header h2rides the title role. It was an untokenizedfont-size: 17pxthat inherited the UA bold 700; now sans 600 with the title tracking, byte-for-byte parity with its sibling.wisdom-card h2. -
.markdown-chart__captionis on the role ladder (body-sm13px), not an off-scale0.85em.e2e guards:
typography.spec.ts"type-system rules hold on sweep-unreachable surfaces (workbench)" + "MB-Web column labels use the mono uppercase voice" — these inject the auth/hover/MB/state-gated surfaces the passive invariant sweeps can't render, so the rules are now guarded there too.
- AI-translate dual columns fill the reader pane. The bilingual view's root
inherits
.markdown-body's single-columnmax-width: 72ch, so the source / translation columns crushed into one measure on the left and left the pane's right half empty..bilingual-colsnow overrides that cap — it fills the reader width (each column gets its own measure) and, past two measures on an ultra-wide pane, caps and centers. Below 1100px, where the pairs collapse to one stacked column, the cap reverts to the single-column 72ch so a stacked paragraph never runs past a comfortable line length. e2e guards:bilingual.spec.ts"dual-column view fills the reader pane…" + "…restores the single-column measure when columns stack…". - DESIGN.md typography frontmatter matched to the implementation. The
machine-readable
typography:block still named the pre-v0.8.0 families (Source Serif 4 / Inter Tight / JetBrains Mono) and carried swapped weights (display 500, label 600) — contradicting the shipped IBM Plex superfamily and the §3 prose table (display 600, label 500). Frontmatter families, weights, and the stray §3 "JetBrains Mono" mention now read IBM Plex.
- MB-Web radii and colors consolidated onto the shared tokens.
src/mb/mb.csskept a parallel light + dark palette whose values already duplicated the workbench tokens. Differently-named locals (--ac/--acb/--border/ …) now alias theirstyles.cssequivalent and same-named ones (--bg/--surface/--text/ …) inherit the global values, so the parallel dark block is gone; the off-scale radii snapped to the4 / 6 / 7 / 8 / 999scale (cards 8px; the badge / chip / filter / toast / sync pills fully round); the note highlighter yellows became named--mb-mark/--mb-mark-stagedtokens. Only the few genuinely MB-specific literals with no shared equivalent (answer-blue, on-accent foreground, AA-darkened faint, hover wash) stay local. Zero-visual-change for colors; verified in the browser across library, reader, and notes in light + dark.
- Settings appearance copy no longer mixes languages. The zh-CN 外观 subtitle
read "System 会跟随操作系统偏好" — the English enum label
Systeminside Chinese chrome, breaking the single-language-chrome rule. Now "「跟随系统」会随操作系统的明暗 偏好自动切换。". - Editorial / responsive heading sizes snapped onto the role ladder. The tail the
0.8.1 small-text pass left —
.wiki-preview-card h2(19 → title 17),.import-done-banner__headline(20 → title 17),.wisdom-popover__title h2(15.5 → body 15), and the responsive.metric-card__valuedownscales (24 / 26 → 22, the reader-h2 editorial step) — is now on the ladder.
- MB-Web type migrated onto the role tokens.
src/mb/mb.cssdropped its self-contained half-step scale (11.5 / 12.5 / 13.5 / 14.5 …); 59 of its 60font-sizedeclarations now resolve to--type-*(label / body-sm / body / title) by nearest role, made available by the app-widestyles.cssimport. The one kept literal is.mb-r-title(22px), the paper-reader title. MB-Web radii/colors remain a tracked follow-up (DESIGN.md §3). Verified in the browser across the library, reader, and notes views.
- Small-text ladder closed (post-v0.8.0 scale consolidation). A residual 12px
tier crammed between
label(11) andbody-sm(13), plus stray 14 / 16 sizes besidebody(15) /title(17), survived the v0.8.0 pass. All 4612pxsites now snap tobody-sm(dense metadata — IDs, paths, durations, pills, table cells, hints, segmented controls) orlabel(the uppercase label voice). The off-scale outliers resolve too:.page-header__description(14 → body-sm),.settings-panel__header h2(16 → title, matching every other panel title), the import section-head titles +.section-title(14 → body),.answer-text(16 → body), and the reader markdown table header/cell inversion (th 12 / td 14 → both body-sm). The small-text band (11–17) on every workbench route now lands on the{11, 13, 15, 17}run — no 12px tier, no stray 14 / 16. A few editorial / responsive heading sizes outside that band (preview-card 19, done-banner 20, wisdom-popover 15.5, responsive metric 24 / 26) are a pre-existing tail and a tracked follow-up (DESIGN.md §3). - Inline / block code (
0.92em) and KaTeX math are documented as deliberate em-relative off-ladder exemptions; a newno UI text renders off the role scalee2e invariant guards the ladder across overview / base / import / chat / settings / tasks / wisdom.
- MB-Web (
src/mb/mb.css) keeps its own half-step subsystem scale for now; migrating it onto the--type-*role tokens is a tracked follow-up (DESIGN.md §3).
- Typeface system → IBM Plex superfamily. The three voices are now IBM Plex
Sans (UI), IBM Plex Serif (reading), and IBM Plex Mono (machine), replacing
Inter Tight / Source Serif 4 / JetBrains Mono. One superfamily means the voices
share a single design DNA — coordination by role, not by foundry — escaping the
ubiquitous Inter default. CJK still falls through to the system fonts; fonts load
from Google Fonts (external, outside the bundle budget).
mb.css(MB-Web) tracks the same swap.
- Serif sprawl reined in (DESIGN.md §3 made honest). The serif had crept onto
metric values, every detail H2, and explorer headers across ~8 ad-hoc sizes while
§3 still claimed it was "reserved for the reader." Serif is now used in exactly
three places — the document reader prose, the assistant reply prose, and the two
stat-number tiers (27px hero, 18px stat). Every panel / card / detail heading
reverts to the sans
titlerole (17px). §3 documents the real policy + the editorial serif sub-scale (32 / 22 / 17 / 27 / 18). - Off-scale chrome + the chat sub-scale snapped onto the six roles. The topbar breadcrumb (12.5px) and connection chip (11.5px) → body-sm; panel titles (14px) → title (17px); the assistant/agent surface (body 14, headings 19/16.5/14.5, code 12.5, blockquote 13.5, role 10.5) folds onto body / serif title-body-bodysm / body-sm mono / 11px label — no private sub-scale. The ~15 distinct font sizes collapse toward the role set.
- Named-rule violations cleared.
trace-pane__title,wiki-backlinksheaders,import-eyebrow, table captions, and the agent role chip were sans tracked-uppercase (the AI "eyebrow" tell) — all moved to the IBM Plex Mono label voice. The reader header path now renders mono (Mono-for-paths). Every mono label shares one tracking (0.04em), retiring the 0.02 / 0.03 / 0.08 / 0.1 spread. - Two-Weight Rule restored on
#trace. Markdownstrong/bis pinned to weight 600; the UA defaultfont-weight: bolderis relative and was compounding to 900 (a fifth weight, past IBM Plex's Bold ceiling) inside already-bold content. - 11px low-vision floor enforced. The 10.5px mono labels (agent role chip, graph
legend, lint fix label, …) move up to 11px; nothing renders smaller. New
tests/e2e/typography.spec.tsinvariants guard the floor, the Mono-Only-Uppercase rule, the three Plex voices, and the panel-title token. Surfaced by the/impeccable critiquetypography pass.
- Typography: the cascade base is now the body type role (15px), not the 16px
UA default.
bodyhad nofont-size, so every unsized element — and everyfont: inheritcontrol (buttons, inputs, selects), the Overview/Settings panel<dd>values, and empty-state titles — silently rendered at the browser's 16px default, bypassing the six-role scale (which puts body at 15px). Settingbody { font-size: var(--type-body-size) }pulls all of it onto the scale. UA-default headings sized by1.5emshift proportionally (~6%; e.g. the task-detail H2 24→22.5px, toward the 22px reader-H2 role) — a deliberate, benign consequence. A newtests/e2e/typography.spec.tsguards the base against regression. Surfaced by the/impeccable critiquetypography pass.
- Overview: loading skeletons, a freshness stamp, and clickable metrics. While
the first
/v1/health|status|infoload is in flight the metric strip and the Runtime/Providers panels now render skeleton placeholders (locking the layout height so data swaps in without a pop-in) instead of misleading zeros; the header carries anUpdated HH:MM/更新于 HH:MMfreshness stamp; and the Data, Knowledge, and Wisdom metrics link to#base,#graph, and#wisdomrespectively (a quiet stretched link — no button chrome). The Refresh button spins and marksaria-busywhile loading but stays clickable, so a stalled request can still be retried.
- Overview metric strip is now a semantic
<dl>. Each metric renders as a<dt>label +<dd>value, so a screen reader announces term/value pairs instead of a flat text stream (matching the Runtime/Providers panels). A failed initial load now shows only the error notice rather than a half-rendered strip.
- Theming: migrated the genuine remaining chip/graph literals to tokens. The
audit's "manual dark-patch block" finding is mostly a false alarm — most of
that block is legitimate dark theming (light
--surface→ dark--surface-2), not redundant literal re-patching. The actual literal re-patches —.frontmatter-chip(#dfe8e5/#f8fbfa/#34423e),.wisdom-chip(#eef3f1/#34423e), and.graph-detail h2/__section strong(#111817/#2d3b37) — now use--surface-2/--line/--text, so they resolve to exactly their old dark-override values (dark mode unchanged, browser- verified) and those four selectors were dropped from the override block. Light chips render as clean recessed warm-stone pills instead of a near-invisible cool near-white.
- a11y: MB-Web view switcher. The 研究/笔记 switcher declared
role="tablist"role="tab"/aria-selectedbut had no tabpanels or roving-tabindex keyboard model. Downgraded torole="group"+aria-pressed(matchingSegmentedControland the mobile panel tabs) so the announced semantics match the actual behavior.
- Accessibility round 2 (from a hardened, anti-hallucination
/impeccable audit). With the earlier rounds' fixes confirmed in place, the audit surfaced genuinely new items:- Label-in-Name (WCAG 2.5.3). Removed hardcoded English
aria-labels that overrode the localized visible label on inputs already inside a<label>/Field(Graph "Hide orphans", Settings Server URL / Token), and localized the two search inputs (Base directory, Graph) via a newsearchLabelcopy key — so the accessible name now matches the visible/zh-CN label instead of announcing English. - Touch targets. Extended the
@media (pointer: coarse)44px block to the chat composer send/stop buttons, Base directory-tree rows, and Wisdom editor controls (were 26–28px on touch). - MB-Web
--faint. Darkened the light-mode token from#95968f(~2.7:1) to#767771(~4.5:1) so the metadata text it colors clears AA.
- Label-in-Name (WCAG 2.5.3). Removed hardcoded English
- Accessibility & contrast pass (from an
/impeccable audit). Resolved the genuine findings of a 5-dimension UI audit:- ARIA state on selection controls.
SegmentedControlis now a namedrole="group"witharia-pressedper option (was className-only, so screen readers couldn't tell which theme/locale was active); the active option is a distinct raised chip vs. a faint hover tint.aria-currentmarks the active sidebar route and the selected master-detail row in Tasks / Trace / Chat.Noticeexposesrole="alert"(errors) /role="status"(info) so post-action banners are announced. The chat composer label issr-only(wasdisplay:none, leaving the textarea unnamed). MB-Web note-editor and popover textareas regained a real focus ring (WCAG 2.4.7). - Dark-mode contrast leaks → tokens. Hard-coded graphite/amber literals
with no dark override (
.wiki-preview-card/__header,.graph-force-control,.citation-item/.event-tape__item/.result-tablebody text,.graph-toggle,.nav-item svg, the#b9d9d3selected-row border, and the.summary-metric--warn/.file-error-cardwarn cluster) now use theme-tracking tokens (--text/--muted/--accent-border-hover/color-mix(--amber …)), so dark mode no longer renders dark-on-dark text or glaring near-white cards. Status-pillok/infotext mixes the signal color toward--textto clear 4.5:1 on its tint; Trace timestamps moved off the sub-AA--subtle. - Touch targets.
@media (pointer: coarse)raises segmented / reader-tab / graph-zoom controls to a 44px tap target (WCAG 2.5.5); desktop is unchanged. - MB-Web mobile reflow. Below 900px the library and Q&A panels were
display:none'd with no fallback, so a phone-opened#MB-Weblink could neither pick a paper nor ask a question. A tab bar (论文库 / 阅读 / 知识问答) now switches which single panel fills the width (WCAG 1.4.10).
- ARIA state on selection controls.
- Badge + segmented-control components (Phase 2 of
docs/ui-refactor-plan.md). AddedSoftLabel,FrontmatterChip, and a genericSegmentedControl(<T extends string>) undersrc/components/and migrated their call-sites: the.soft-labelcount/layer/anchor badges across Base, Graph, Wisdom, and Tasks; the.frontmatter-chiptag/source chips in the Markdown reader and the Base info panel; and the Settings theme/locale toggles. The wrappers are DOM-identical (same element, classes, and passthrough attributes), so the e2e console gate and layout are unchanged.StatusPillstays separate and the three CSS classes are not merged; three block uses of.soft-labelon a<p>(hints / empty state) stay raw markup by design. - Scattered color literals → design tokens. Migrated the six hand-picked hex
literals to the DESIGN.md Intent-Mapping ramp: the nav / segmented /
secondary+icon hover borders (
#cbd8d4/#c5dad5/#9fb0aa) →--accent-border-hover; the nav resting text (#3c4a46) →--muted; the nav strong label (#1d2926) and the panel + section titles (#25322f) →--text. Collapsed the dead duplicate.page-header h1rule (the survivor now consumes--type-title-page-*) and dropped three now-redundant dark-modecoloroverrides whose light base already tracks the theme. The only intended visual change is the nav resting label rendering one step lighter (#3c4a46→--muted); active markers, fills, and hover/active states are unchanged. Verified route-by-route in light + dark against a livedikw-core.
- Shared control components (Phase 1 of
docs/ui-refactor-plan.md). AddedButton,IconButton, andField(plus acxclass-joiner) undersrc/components/and migrated every hand-rolled.primary-button/.secondary-button/.icon-button/.fieldcall-site across 14 page files to them. The wrappers are DOM-identical (same element, classes, and attributes), so behavior, the e2e console gate, and layout are unchanged — the change is −104 net lines of duplicated markup.Buttondefaultstype="button"(no accidental form submits) and exposesvariant="primary|secondary|danger";IconButtonrequires alabel(→aria-label) so every icon button keeps an accessible name;Fieldmapsgrow/small/inline/tokento the existingfield--*modifiers. Segmented controls are deferred to a later phase.
- Design-system token foundation.
src/styles.cssgains additive token groups, all documented inDESIGN.md: a 4px spacing grid (--space-*, with 6/10/14px half-steps), a four-step motion scale (--dur-fast|state|popover| overlay+--ease-standard|emphasis), six typography roles (--type-<role>-{size,lh,ls}across the serif/sans/mono voices), andcolor-mixalpha layers (--line-alpha,--overlay-hover|active,--accent-border-hover) for borders/hover fills that track the theme over any surface. They are defined now and consumed incrementally — the additions alone change no rendering. docs/ui-refactor-plan.md— the phased UI/UX refactor roadmap (token foundation → shared controls → badges/segmented → panels/MB-Web), learned from Vercel's Geist system while keeping the warm-stone + petrol identity.
- Destructive control text was invisible.
--dangerwas referenced by.secondary-button--dangerand the session...menu's delete item but never defined insrc/styles.css, so the text silently fell back to the inherited body color. It is nowvar(--red)(defined once, tracks the theme through--red), so destructive affordances actually read red in both light and dark.
DESIGN.mdrestructured to a Geist-informed section order — Intent Mapping table, six Type Roles, a Spacing & Rhythm section, a Motion section, a formal radius scale, per-component spec tables, and a new bilingual Voice & Content section — reconciled to the real shipped values (7px control radius, sans page titles, borderless pills) and kept on the "Quiet Instrument" identity.docs/ui-system.mdanddocs/ui-checklist.mdupdated to match.
- Chat: Stop is shown only while a reply streams. The idle composer no longer renders a disabled, glyph-less Stop control next to Send (it read as an empty placeholder square). Stop now appears only while a reply is streaming — an honest surface with no dead control.
- Tasks: removed card-in-card nesting in the detail panel. The summary tiles
(
created/duration/digest) and the Result metric tiles were bordered boxes nested inside the bordered detail panel. They are now borderless recessed tonal wells, and theresult-summarygrouping is seamless on the panel surface; the semantic--warn(amber) and--error(red) borders are kept explicit. Verified in light and dark against a live core.
- Chat: send on Enter. The composer now sends on
Enterand inserts a newline onShift+Enter, matching the dominant chat convention (previously only the Send button submitted). The handler skips while an IME is composing (nativeEvent.isComposing) so confirming a Chinese candidate with Enter never sends mid-composition.
- Retrieve layout collisions. Page Refs rendered the page title and layer
label with no separator (
Knowledgeknowledge); the layer is now a muted mono tag in a flex row. In the Chunks table a long unbreakable path overflowed its grid track and collided with the Seq column; the path cell now shrinks and wraps (min-width:0; overflow-wrap:anywhere). - Reader line length. Prose ran edge-to-edge on wide panes;
.markdown-bodyis now capped to 72ch (DESIGN.md body 65–75ch). Narrower surfaces (chat replies, bilingual columns) already sit under this width, so it's a no-op there. - Wisdom empty state. An empty wisdom base showed "No wisdom pages match" (implying a filter miss); it now distinguishes the true zero-state ("No wisdom pages yet" + a hint to use New or run synth) from a filtered no-match.
- MB-Web identity drift. MB-Web shipped its own green palette (forest-green
accent, green-tinted neutrals); its
mb.cssneutrals, accent, and danger are realigned to the workbench warm-stone + petrol tokens (One Indicator Rule), keeping the answer-blue semantic. - MB annotation side-stripe.
.mb-annoused the banned 3px accentborder-leftcallout pattern; reworked to a full accent-tinted border. - Copy polish. Graph stats read "{n} links" (was singular); the Retrieve "No page refs" empty state gained a teaching detail line.
- Single-source keyboard focus from the accent token (
src/styles.css). Two new tokens —--focus-ring(a soft 3px box-shadow ring for text fields) and--focus-outline(a crisp 2px solid outline for other controls) — both derived from--accentviacolor-mix, so the focus indicator now tracks light/dark automatically. A zero-specificity:where(button, a[href], [role="button"], summary, [tabindex]):focus-visiblerule gives buttons, links, and other interactive controls a consistent branded keyboard-focus ring (WCAG 2.4.7), while existing per-element focus styles (graph nodes, reader controls, resizer) still win. Fixes a dark-mode defect where text inputs rendered a hardcoded light-petrol focus ring (rgba(11,124,112,…)) that was nearly invisible on the dark surface. Seedocs/ui-system.md.
- Impeccable design baseline + UI audit backlog.
PRODUCT.md(strategic: register, users, brand personality, anti-references, principles) andDESIGN.md(visual: tokens, typography, components, do's/don'ts, in Stitch format) capture the design system, with.impeccable/design.jsonas the machine-readable sidecar.docs/ui-audit-backlog.mdrecords findings from a full live-core interaction walkthrough (light + dark) for follow-up.
- Auto-track new dikw-core releases (
.github/workflows/bump-dikw-core.yml). Weekly (and on demand) the workflow resolves dikw-core's latest GitHub release and, if it's newer than the pinnedDIKW_CORE_VERSION, opens achore/bump-dikw-core-<version>PR editing both pin sites (live-integration.yml+scripts/live-core/harness.mjs) and labels itlive-integrationso the full real-core verification runs on the PR. The pin stays explicit + reviewed (reproducible nightly); upgrades are surfaced automatically instead of silently followed. Needs aDIKW_BUMP_TOKENfine-grained PAT (contents + pull-requests write) — the defaultGITHUB_TOKENcan't open a CI-triggering PR; without the secret the job no-ops with a warning. Seedocs/integration-verification.md.
- Live integration verification (
npm run live:verify). End-to-end verification of the working tree against a realdikw-core(the published GHCR image, e.g.ghcr.io/opendikw/dikw-core:0.6.1) backed by Postgres/pgvector — the gap the rest of the stack can't cover (Playwright mocks/v1;smoke:coreneeds a core you seeded yourself). One command boots core + Postgres on dynamic ports under a unique compose project (so multiple core versions coexist), seeds it through dikw-web's own write pipeline (import → ingest → synth → lint, reusingbuildImportBundle+DikwClientviatsxso the wire shape can't drift), then verifies the read contract (smoke-core), the browser read routes (a new opt-in Playwrightliveproject on real data with the console gate), and the agent↔core path (a real chat turn must invoke a core-backed tool, read off the agent's observable event stream). New:docker-compose.live-core.yml,scripts/live-core/*,scripts/seed-core.mts,tests/fixtures/live-base/,tests/e2e/live/,.env.core.example, and a non-gating.github/workflows/live-integration.yml(dispatch / nightly /live-integrationlabel). Sub-commands:live:up/live:seed/live:smoke/live:down. The defaultverifygate is untouched. Seedocs/integration-verification.md.
- README polish. Added a centered header (logo, tagline, status + tech
badges) and a compact section nav; pointed the
dikw-corelink at the public GitHub repo instead of a sibling-checkout relative path. - Corrected "read-only" doc drift against the code.
dikw-webis no longer read-only overdikw-core: ImportPage and MB-Web upload papers (/v1/import→ ingest → synth), the Wisdom editor saves/favorites (POST /v1/base/wisdom), the Tasks page fires maintenance ops, and the agent proposes maintenance. Dropped the inaccurate whole-app "read-only" framing from the GitHub About,README.md,CLAUDE.md,CONTEXT.md,docs/core-contract.md, anddocs/ui-system.md. Genuinely read-only contexts (the Graph view, the reader's Read/Frontmatter tabs, the top-bar connection strip) keep the wording. - Genericized the white-label branding example.
public/config.example.json, the README branding snippet, and the branding tests now use neutral placeholders (Acme-DIKW/示例知识库) instead of a real downstream brand name. No runtime behavior change — the default brand staysOpenDIKWandconfig.jsonremains gitignored per-deployment.
- Clear the OpenTelemetry moderate advisories (12 of them). Bumped the
experimental OTel deps we import directly (
@opentelemetry/api-logs,exporter-trace-otlp-http,sdk-logs) to^0.219.0, and addedoverridesforcing the stable OTel packages (@opentelemetry/core,resources,sdk-metrics,sdk-trace-base,sdk-trace-node) to^2.8.0tree-wide so the copies pulled transitively by@google/adkare also patched — clearing the@opentelemetry/core <2.8.0W3C Baggage unbounded-memory advisory and every advisory chained to it. Also overrodedompurifyto^3.4.11(theIN_PLACE/SAFE_FOR_TEMPLATESXSS fixes) for the copy pulled bymermaid. The remaining moderate advisories (uuid/googleapis/protobufjs/@google-cloud/*) are all transitive via@google/adk's unused GCP exporter path and have no clean fix (npm's only "fix" is a@google/adkmajor downgrade to 0.1.3) — left as accepted risk. None are reachable in dikw-web's runtime and none gate the prod high+ audit. - Drop npm from the production image. The runtime stage only ever runs
node dist-server/standalone.mjs, so npm is removed (rm -rfof/usr/local/lib/node_modules/npm+ thenpm/npxbins). This clears the Trivy HIGH for the npm-bundledundici < 6.27.0(CVE-2026-12151, WebSocket fragment-count DoS) — whichnpm i -g npm@latestwould not fix, since the latest npm still bundles undici 6.26.0 — and keeps future npm-bundled-dep CVEs out of the image entirely.
- Retry a failed PDF/Office conversion in place. A failed conversion row in
the import
convertingstage now offers a Retry button beside Skip that re-runsconvertSourcefor just that file — no re-selecting or re-uploading the whole batch. A single transient failure (e.g. a mineru quota/5xx hiccup that fails every file at once) can now be recovered per row.
- Conversion no longer auto-advances on partial success. Previously, once any
file converted the batch jumped straight to the bundle preview and silently
dropped the still-failed files. Now a still-failed row keeps the
convertingstage open (with Retry/Skip) until it is resolved; the batch advances to bundling only once every row is converted or skipped with at least one success. Skip now drops only that one row.
- MB-Web appearance unified with the workbench. MB-Web no longer keeps a
standalone theme preference (
dikw-mb.theme); it now reads the samedikw-web.themekey the workbench Settings appearance panel writes, resolving a storedsystempreference to light/dark via the sharedresolveTheme(lifted intosrc/i18n.ts) and following live OSprefers-color-schemechanges while it stayssystem(parity with the workbench). MB-Web keeps its one-tap header light/dark toggle, but the toggle now commits an explicit light/dark to that shared key — so a flip in MB-Web is reflected in the workbench and vice versa, and switching between#MB-Weband the workbench no longer flips the theme between two independently stored values. Opening MB-Web (or the OS flipping while it's open) never overwrites a storedsystempreference (it applies the resolved value to the DOM but only persists on an explicit toggle).
- MB-Web's standalone
dikw-mb.themelocalStorage key and its private theme initializer — superseded by the shareddikw-web.themeread above.
- Connection settings unified into the workbench Settings page. The Server URL
- Token now commit on an explicit Save (replacing the previous keystroke-by-
keystroke write), with an inline "unsaved changes" hint and a "Saved" confirmation
beside the button; the existing clear-connection button is renamed Clear and
now resets to the default URL + empty token immediately. The connection
persists to
localStorageinstead of per-tabsessionStorage, so a saved connection is shared across tabs and survives a browser restart — the bearer token is therefore at rest inlocalStorageby default. The shared connection keys + default URL are single-sourced insrc/config/connection.ts.
- Token now commit on an explicit Save (replacing the previous keystroke-by-
keystroke write), with an inline "unsaved changes" hint and a "Saved" confirmation
beside the button; the existing clear-connection button is renamed Clear and
now resets to the default URL + empty token immediately. The connection
persists to
- MB-Web reads the connection instead of owning a settings panel. Its header gear
and the actionable library-load error CTA both navigate to the workbench
#settingspage; a cold-opened#MB-Weblink recovers the connection from the sharedlocalStoragewithout a per-tab handoff (the #97 cold-open fix, now via shared storage rather than an opt-in mirror). - Page titles use the UI sans family.
.page-header h1no longer renders in the editorial serif (which fell back to a serif CJK face, clashing with the sans body); page chrome now reads as one cohesive type family. Reader / card headings and big metric numbers keep the editorial serif.
- The MB-Web connection panel (
MbConnectionPanel) and its opt-indikw-mb.rememberConnlocalStorage mirror — superseded by the unified Settings page + always-onlocalStoragepersistence above.
- MB-Web could not reach
dikw-coreon a cold open (#97). The#MB-Webvariant read the core Server URL + Token only from per-tabsessionStorageand shipped no settings UI of its own, so a shareable#MB-Weblink opened in a fresh tab (or after a browser restart) silently fell back tohttp://127.0.0.1:8765+ an empty token and showed a generic "论文库加载失败" with no way to recover. MB-Web now:- has its own connection settings panel (Server URL + Token), reachable from a header gear and from the library error CTA — so a cold-opened link can be configured/recovered without bouncing through the workbench Settings page;
- can opt in to "记住连接", mirroring the connection to
localStorage(dikw-mb.rememberConn) so it survives a tab close / browser restart; off by default, so the bearer token only lands at rest when the user explicitly enables it; - replaces the generic library error with an actionable one that distinguishes a 401/403 auth failure from an unreachable core, each with a button into the panel; and
- reports an honest 未连接 · dikw-core header state instead of a stuck "连接中…".
- 论文知识库 (MB-Web) reading variant — a focused paper-reading UI shipped in
the same bundle.
src/main.tsxnow renders a top-levelRootthat mounts the newMbApp(src/mb/) for the#MB-Webhash and the original multi-page workbench (App) for every other hash (#chat/#base/ …), so both ship together (shareable linkhttp://127.0.0.1:4321/#MB-Web);Apponly rewrites its own legacyquery→chathash, so the two never fight over the URL. The variant is read-only overdikw-coreand reuses the existing/v1,/agent, and/web/translate+/web/mineruinfra; its UI lives undersrc/mb/with.mb--scoped styles (no new framework), andApp+pages/are untouched. Features: a three-column resizable + collapsible workspace; a Source-layer paper library with browser-side rename aliases; a reader with 原文 / 中英对照 (paragraph-aligned bilingual via/web/translate, persisted per paper); per-paper agent Q&A (chat history kept per paper); 我的笔记 (localStorage card list + detail page, with a delete-confirmation dialog) mirrored to the Wisdom layer (POST /v1/base/wisdom); and upload (PDF/Office → MinerU → import → ingest → synth) with optional pre-translation at import. - Observability demo stack + docs — Phase 7 (final) of OTel observability. A new
docker-compose.observability.yml(separate from the productiondocker-compose.yml, which it does not touch) brings up an OTel Collector that fans OTLP into Jaeger (traces) + Prometheus (metrics) + Loki (logs), all visualized in Grafana with datasources pre-provisioned.otel-collector-config.yamlwires the three pipelines (OTLP in → Jaeger / Prometheus-scrape / Loki, CORS-enabled for the browser RUM exporter);observability/prometheus.yml+observability/grafana-datasources.ymlmake the stack work on firstup. Defaultupis backends-only (no LLM creds needed);--profile appalso builds + runs the sidecar pointed at the in-network collector (creds from the operator's.env, never inlined — placeholder secrets only, gitleaks-clean). Newdocs/observability.mddocuments the fullOTEL_*env reference (incl.OTEL_EXPORTER_OTLP_HEADERSfor Grafana Cloud / Honeycomb / Datadog), the env-only sampling limitation, thedikw.*metric catalog, the browser-RUMconfig.jsonblock + its credential-exposure security model, and the privacy posture;.env.example,README.md, andCLAUDE.mdcross-reference it. No app behavior change. - Browser RUM (opt-in frontend traces) — Phase 6 of OTel observability. A new
src/telemetry/initBrowserOtel.tsboots the OpenTelemetry web SDK (WebTracerProvider+OTLPTraceExporter, with document-load + fetch instrumentations) so the React app emits RUM spans underservice.name=dikw-web-browser. The fetch instrumentation propagates a W3Ctraceparentto same-origin requests — always the sidecar's/agent+/web, stitching a browser span to the sidecar's Phase 2 SERVER span; a cross-origin core (/v1in the standalone deployment) gets no header and no CORS preflight, and the exporter's own collector POSTs are excluded viaignoreUrls. Fetch-span URLs are redacted before export — the query string is dropped (it can carry user-derived values such asoriginalFilenameandinputSha) and high-cardinality id path segments are templated (/web/mineru/jobs/:id,/v1/base/pages/:id), mirroring the sidecar's privacy-minimizing SERVER-span posture (serverRoute); this runs in anonEndingspan processor so every fetch span (success, error, abort) is covered, not just the defaulturl.full/http.urlraw export. The user-interaction instrumentation is deliberately not registered: it only wraps listeners added after it loads, but React attaches its delegated listeners at render — before the async, lazy SDK init resolves — so it would miss them, and the fix (loadingzone.js, which globally patchesPromise/setTimeout/requestAnimationFrame, before React + Pixi mount) is an unjustified risk for click-span coverage; the defaultStackContextManageris enough for fetch propagation. It is opt-in and default-off: a newsrc/config/telemetry.tsreads atelemetry.endpointfrom the runtimepublic/config.json(mirroringloadBranding()); with no endpoint configured the init is a no-op and — because the SDK is loaded via dynamicimport()— none of it is downloaded. The OTel web SDK therefore stays out of the entry bundle (it lands in a lazy chunk);scripts/check-bundle.mjs'stotal JSceiling was raised 1900 → 1950 KB to count that opt-in chunk while restoring the pre-RUM headroom (the entry budget is unchanged). RUM init is best-effort — any failure is swallowed so it can never break the app or add console noise (the e2e console gate is unaffected).public/config.example.jsonships thetelemetryblock with an empty endpoint (disabled) so copying it for branding never silently enables telemetry. New browser runtime deps:@opentelemetry/sdk-trace-web,@opentelemetry/exporter-trace-otlp-http, and@opentelemetry/instrumentation-{fetch,document-load}(@opentelemetry/apistays single-deduped at 1.9.x). RUM does not touchdikw-core; browser logs/metrics and user-interaction spans are noted follow-ups (RUM convention is traces first). - OpenTelemetry outbound CLIENT spans (opt-in) — Phase 5 of OTel observability.
A new
server/agent/instrumentation.tsregisters@opentelemetry/instrumentation-undiciso the standalone sidecar's outboundfetch/undici calls (dikw-core/v1, MinerU, Tavily/Jina, the translator's Anthropic SDK) emit OTel CLIENT spans and inject a W3Ctraceparent, continuing the inbound SERVER-span trace through to the upstream. Registration is gated on an OTLP traces endpoint (OTEL_EXPORTER_OTLP_ENDPOINT/*_TRACES_ENDPOINT): with no endpoint env the HTTP layer is not patched at all, so outbound behavior is byte-identical to before. The patch is via undici'sdiagnostics_channel(dispatcher-agnostic). The query string is redacted from every CLIENT span (startSpanHookkeepsscheme://host/path, setsurl.queryto[REDACTED]) so MinerU's presigned upload/result URLs — whose query carries a bearer credential — never reach the OTLP backend. CLIENT spans (like SERVER spans) are filtered out of the in-memory#tracestore (DikwSpanProcessornow keeps onlySpanKind.INTERNAL, ADK's own kind), so the#tracewaterfall stays agent-only; the spans still export over OTLP.@opentelemetry/instrumentation@^0.219.0@opentelemetry/instrumentation-undici@^0.29.0are added as runtime deps (@opentelemetry/apistays single-deduped). Note: a fetch using a per-calldispatcherfrom the npmundicipackage (thetools.tsHTTPS_PROXYProxyAgentpath) is incompatible with Node's built-infetchindependently of this change (a pre-existing undici-version mismatch); the instrumentation is orthogonal to it.
- Structured sidecar logging + OTel logs bridge — Phase 4 of OTel observability.
A new
server/shared/logger.ts(createLogger(scope)) replaces the sidecar's ~21 ad-hocconsole.*calls with one-line structured records: JSON to stdout (a human text line when stdout is a TTY orDIKW_LOG_FORMAT=text), the active span'strace_id/span_idinjected for trace↔log correlation, and a mirrored OTelLogRecord. WhenOTEL_EXPORTER_OTLP_ENDPOINT(orOTEL_EXPORTER_OTLP_LOGS_ENDPOINT) is set, ADK'smaybeSetOtelProvidersbuilds theLoggerProvider(its own OTLPBatchLogRecordProcessor), so logs ship to Loki/etc. alongside traces and metrics under the sameservice.name=dikw-webresource; with no endpoint env there is no provider and the bridge is a no-op (stdout output only). Field names matchingkey|token|auth|secret|password|credentialare redacted to[redacted]as defense in depth, and there is no arbitrary-object dump path, so a secret can't be logged by accident;Errorfield values are reduced toname: message(no stack dump). This is a visible stdout format change fornpm start— setDIKW_LOG_FORMAT=textfor the prior human-readable style.@opentelemetry/api-logsis a new runtime dep (the emit API);@opentelemetry/sdk-logsis added for the test harness (both pinned to ADK's resolved0.205.0). Logging never touchesdikw-core. - OpenTelemetry metrics — Phase 3 of OTel observability. A new
server/shared/metrics.tsrecords six instruments on the global meter:http.server.request.duration(histogram, seconds;http.request.method/http.route/http.response.status_code, recorded as each SERVER span ends),dikw.job.duration+dikw.job.count+dikw.job.inflight({dikw.job.family = mineru|translate, dikw.job.outcome = succeeded|failed}, recorded at the detached conversion/translation runner boundaries — inflight is an UpDownCounter toggled+1/-1across the run),dikw.llm.tokens({gen_ai.token.type = input|output}, from the token countsDikwSpanProcessoralready parses), anddikw.agent.turn.duration({dikw.agent.turn.outcome = ok|aborted|error}, around onerunMessage). Instruments bind lazily on first record, so they pick up the MeterProvider ADK'smaybeSetOtelProvidersinstalls whenOTEL_EXPORTER_OTLP_ENDPOINT(orOTEL_EXPORTER_OTLP_METRICS_ENDPOINT) is set — metrics then export over OTLP via ADK'sPeriodicExportingMetricReader, alongside traces, sharing theservice.name=dikw-webresource. With no endpoint env there is no MeterProvider and every record is a no-op (no behavior change).@opentelemetry/sdk-metricsis added for the test harness only (pinned to ADK's resolved2.7.1); the sidecar never imports an exporter. Metrics never touchdikw-core. - OpenTelemetry inbound SERVER spans — Phase 2 of OTel observability. Every
/agent/*and/web/*request is wrapped in a route-templated SERVER span (server/shared/withServerSpan.ts): it extracts inbound W3C trace context (so a browser → sidecar trace stitches through), runs the handler inside that span's context (the ADK invocation / future outbound spans nest under it), and ends on responsefinish/closewith thehttp.request.method/http.route/http.response.status_codesemantic-convention attributes. Session and job ids collapse to:id(and proposal ids to:proposalId) so span names /http.routestay low-cardinality; the raw request path is deliberately not exported asurl.path(it would re-introduce those ids into span data —http.routeis the aggregation key). SERVER spans export via OTLP (when an endpoint is configured) but are filtered out of the in-memory#tracestore (DikwSpanProcessorskipsSpanKind.SERVER; ADK agent spans are allINTERNAL), so the#tracewaterfall is unchanged. Because the SERVER span now parents ADK's rootinvocationspan,SpanStore.getSessionTracesnormalizes any parent absent from the served view (the filtered SERVER span, or a FIFO-evicted parent) back tonull, so the invocation stays a true root and theTraceSpanViewcontract holds. No new dependencies; no change to the#traceDTO or any/agent/*wire format. - OpenTelemetry trace export (opt-in) — the first phase of OTel observability.
Agent spans now carry a
service.name=dikw-webresource (server/agent/telemetryResource.ts;service.versionfrompackage.json, a per-processservice.instance.id, overridable via the standardOTEL_SERVICE_NAME). SettingOTEL_EXPORTER_OTLP_ENDPOINT(orOTEL_EXPORTER_OTLP_TRACES_ENDPOINT) makes ADK additionally export these spans over OTLP to any collector/backend (Jaeger, Grafana Tempo, Honeycomb, …), with sampling honoringOTEL_TRACES_SAMPLER/_ARG. With no endpoint env the spans stay in the in-memory#tracestore only — behavior-identical to before — and OTLP-exported spans inherit the same content-free redaction (ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS=false).@opentelemetry/resources+@opentelemetry/semantic-conventionsare promoted from already-hoisted transitive deps to direct deps (pinned to ADK's resolved versions; no new physical install). No change to the in-memorySpanStore, the#traceDTO, or any/agent/*wire format. - Automated release tags — a
releasejob in CI (.github/workflows/ci.yml) cuts a GitHub Release taggeddikw-web-v<package.json version>after the full CI gate is green on a push tomain. It is idempotent (skips when the tag already exists), so only an actualpackage.json.versionbump produces a new tag — which makes "every version update gets a tag" fall out for free; the current0.3.0is tagged on the first run. Release notes are pulled from the matching CHANGELOG section byscripts/changelog-notes.mjs. Job-scopedcontents: writekeeps the workflow-level read-only default everywhere else. No new dependencies. - Sidecar translation endpoint (
/web/translate,server/web/) — the backend half of Base-reader bilingual reading. A job + poll API (mirroring mineru, issue #60):POST /web/translate/submit({ blocks, targetLang }, the document's markdown split into text blocks) →202 { jobId }, then pollGET /web/translate/jobs/{id}and fetch block-aligned{ blocks: [{ i, tr }] }from…/result(…/cancelaborts). The text blocks are translated in ordered batches (splitIntoBatches), one streaming@anthropic-ai/sdkcall per batch over MiniMax (messages.stream(...).finalMessage(); deliberately not the ADKMiniMaxLlmadapter, which is bound to ADK'sBaseLlminterface). After each batch the runner publishesprogress: { done, total, blocks }to the poll status, so the browser reveals paragraphs progressively instead of waiting for the whole document; per-batch timing is logged. The model wire protocol is a delimiter — blocks joined by a distinctive sentinel line, reply split back on it — not a JSON array: arbitrary scientific Markdown (LaTeX backslash commands like\circ, unescaped quotes around code identifiers like"scikit-learn", brackets) round-trips verbatim instead of corrupting a JSON array (both modes were live-observed failing the JSON protocol on a real paper). A wrong block count from the model is reconciled by splitting the batch and re-translating the halves down to singletons (not a re-ask — the miscount is often deterministic); two model failure modes a matching count hides are self-healed byrepairBlocks(Chinese target, one re-ask per block): a block echoed back untranslated (≥ 6 English words, no CJK), or one whose translation contains its source verbatim (bilingual echo) or is > 2× the source length above a 60-char floor (EN→中 compresses, so this flags hallucinated/continued content — live-observed on test2.md). The flagged block is re-asked alone and the re-ask re-validated: a result still oversized/echoed is rejected for the source text rather than injecting bloat. The system prompt also forbids adding/continuing/summarizing/inventing content. The SDK's ownmaxRetriesis 0 so retry policy is single-sourced inTranslatorClient.translate, which retries retryable transport faults (429 / timeout / 5xx / connection) and an empty reply with exponential backoff + jitter; a truncated (max_tokens) reply and aborts are not retried. Wikilink targets are server-side re-pinned by order (repinWikilinks) so a translated[[target|label]]keeps its destination even if the model rewrites it. Submit enforces a 4 MB / 2000-block cap. Reuses the chat agent's MiniMax credentials (DIKW_AGENT_API_KEY/_BASE_URL/_MODEL, no dedicated key); only the per-call output capDIKW_WEB_TRANSLATOR_MAX_TOKENSstays translator-specific; missing key →503 translate_disabledandGET /web/translate/health→{ enabled: false }(the reader hides its AI 翻译 entry). Browser clientsrc/utils/translate.tsadds an IndexedDB cache (dikw-translate-cache, 7-day TTL sweep) keyed by aTRANSLATE_VERSIONthat is bumped when translation logic changes so stale pre-fix translations are dropped. The reader UI lands in a follow-up. No new dependencies. - Base reader bilingual reading (
#base) — the reader half of the feature above. English pages (detected after fetch via CJK ratio) gain an AI translate toggle fused onto the Read tab (aria-pressed, hairline seam) when the sidecar translator is configured; Chinese pages and a disabled translator show no toggle. Toggling on splits the body into blocks, sends the text blocks to/web/translate, and renders a paragraph-aligned dual-column view (BilingualView): source markdown left, Chinese right, with special blocks (code / tables / charts /$$math / rules / standalone image lines) rendered once and centered, never translated — a figure is no longer duplicated and alt-translated across both columns, even when a hard line break joins it to its caption (the MinerU figure shape). Translated paragraphs stream in progressively as the sidecar's batches land (each pair shows a skeleton only until its own translation arrives), rather than appearing all at once. A status bar exposes translating / cached / re-translate / cancel; narrow screens stack the columns. The shared markdown renderer was extracted fromMarkdownViewintomarkdown-runtime(mono and dual-column render through the exact same markdown-it instance, sanitizer, and chart / mermaid / image hydration); the translated column renders through a prefixed heading-slug env so its ids can't collide with the source column's. Clicking a wikilink in the translated column previews the target page with its title + summary translated to Chinese (usePreviewTranslation— two tiny blocks through the same/web/translate+ IndexedDB cache — with anAIbadge on the card and a silent fallback to the original on failure); a source-column or mono-view click previews the original, and an already-Chinese target is never re-translated. No new dependencies; seedocs/adr/0003-bilingual-reading.md. - ESLint gate (
npm run lint→eslint.config.js, flat config,--max-warnings 0, inverify+ CI). Covers the lint layertsc --strictmisses: React hook dependency/order rules, unused symbols (tsconfig has nonoUnusedLocals), and no rawconsolein the shipped browser bundle. react-hooks is pinned to its two classic rules (rules-of-hooks + exhaustive-deps) rather than the v7recommendedpreset, whose opinionated rules (set-state-in-effect, immutability, …) would force a non-surgical refactor of working code; type-checked rules are omitted to keep it fast. Pre-existing violations were resolved — dead types/imports removed, two useless assignments and an over-escaped regex fixed, and genuinely intentional hook-dep omissions annotated with a reason. - Prettier (
npm run format/format:check, inverify+ CI) across code (.ts/.tsx/.js/.mjs/.css/.json); a one-time repo-wide reformat landed in its own commit (printWidth100 to match the codebase). Markdown is excluded (.prettierignore) — prettier's prose pass is cosmetic churn (e.g.*italic*→_italic_) with no correctness value on the hand-maintained docs. - Layout-shift (CLS) perf budget (
tests/e2e/perf.spec.ts, ridestest:e2e). Asserts Cumulative Layout Shift ≤ 0.1 on the primary routes (overview / base / graph / tasks / chat / wisdom) — the one Core Web Vital stable under headless Chromium. LCP and long-task totals are measured and surfaced as Playwright annotations but never gate (runner-dependent timing), the same reasoning that keepssmoke:coreout of CI. 2026-06 baselines: CLS 0.00–0.05 across all six routes. - Bundle-size budget gate (
npm run check:bundle→scripts/check-bundle.mjs, wired into CI after the verify gate). Asserts gzipped ceilings for the entry JS (index-*.js), total JS, and CSS againstdist/, catching the "a heavy lib crept into the bundle" regression class that typecheck/tests can't see. Budgets are inline with deliberate headroom over the 2026-06 baseline (entry 243 / total 1695 / CSS 25 KB gzip) and raised consciously like the coverage thresholds. The CSS ceiling doubles as a backstop for the "no UI framework" rule. No new deps. - Review rubric (
docs/review-rubric.md) — the project-specific lens for the pre-merge independent review (Delivery Loop step 6): simplicity-first / surgical / TDD, the repo traps generic reviewers miss (server/**.jsimports, no UI framework, token never exposed, single-language chrome), and the don't-touch list. Wired intodikw-web-delivery-workflowandCLAUDE.md. - Live-core contract smoke (
npm run smoke:core→scripts/smoke-core.mjs, driven by the newdikw-web-smoke-coreskill). The e2e suite mocks/v1entirely, so it can't see real contract drift; this script asserts the consumed subset ofdocs/core-contract.md(thewiki → knowledgelayer value, the/v1/tasksenvelope,PageReadResult.frontmatter, and the health/status/graph shapes) against a reachable core and exits non-zero on drift. Intentionally outside CI (needs a live core); Nodefetchis proxy-immune for localhost. Validated green against a live dikw-core 0.5.1. No new dependencies. - E2E console gate. Every Playwright spec now imports
test/expectfrom a newtests/e2e/harness.tsinstead of@playwright/test; the harness fails any test that emits aconsole.erroror an uncaughtpageerror, turning "the console stayed clean" from a manual review step into a deterministic gate. Resource-load 404s (the suite intentionally 404sconfig.jsonand a missing asset) andAbortErrorare allowlisted; a test that deliberately drives an error path opts out withtest.use({ consoleGuard: false }). - Project verification skills (
.claude/skills/, now tracked in git via a.gitignoreexception while local settings stay ignored):dikw-web-verify-frontendencodes the per-route browser-verification pass (Delivery Loop step 5) with the project's hard-won gotchas (graph canvas needs--headed, not Chrome MCP;requestIdleCallbackpaths only hit the timeout in an MCP tab), anddikw-web-delivery-workflowmakes the Delivery Loop an executable orchestration of the existing skills. docs/ui-checklist.md— a pass/fail rubric for the qualitative UI rules (single-language chrome, dark reader contrast, small radii, no UI framework, graph filters/legend/no-bloom, the markdown HTML allow-list), run by thedikw-web-verify-frontendskill. Test/tooling/docs only — no runtime, wire, or contract change, so no version bump.- App sidebar collapses to an icon rail (
src/App.tsx). A footer toggle (PanelLeftClose/PanelLeftOpen) shrinks the left sidebar to an icon-only rail; nav labels are visually hidden but kept for assistive tech (native tooltips restore the name on hover), and the choice persists inlocalStorage(dikw-web.sidebarCollapsed). The rail is desktop-only (gated above the 900px breakpoint); on narrow screens the sidebar keeps its existing behavior. - Base directory panel is resizable (
#base). The directory-tree column can be dragged wider/narrower via a splitter in the layout gap (pointer-capture, so the drag tracks past the handle) or stepped with the arrow keys on a focusablerole="separator"(Home/End jump to the bounds), clamped 240–640px and persisted (dikw-web.wikiSidebarWidth), so long filenames fit. Hidden on narrow screens.
- Bilingual reader keeps an in-flight translation on toggle-off — shared
useBilingualReader.toggle()no longer aborts the running translation when switching back to the source view; it lets the job finish and write the IndexedDB cache, so toggling 原文 ↔ 中英对照 (in both the Base reader and the new MB reader) no longer restarts the translation from scratch every click.BilingualPaper(MB reader) is nowReact.memo'd so a parent re-render doesn't rebuild the dual-column DOM and drop an active text selection / staged highlight. - Extracted the dotenv helpers shared by both sidecar config loaders
(
parseEnv/readEnvFile/readOptional) into a newserver/shared/env.ts;server/agent/config.tsandserver/web/config.tsnow import them instead of each carrying a byte-equivalent private copy. Behavior-neutral internal refactor — no env var, wire format, orloadAgentConfig/loadWebConfigcontract changed, so no version bump. - Base reader "AI translate" control is now an on/off switch (
role="switch"aria-checkedwith a sliding knob) rather than a plainaria-pressedbutton — a clearer affordance for the EN→中 dual-column toggle. Behavior unchanged.
- Base reader tab rename: "Info" → "Frontmatter" (zh-CN 信息 → 元信息), matching
what the tab shows (
PageReadResult.frontmatter). The en source-tab label stays "Source"; only the zh-CN label changed (原 "源码" → "原文"). - Base reader raw-source panel restyled. The source tab dropped the harsh
black-on-white block for the reader token surface (
--reader-*, dark-mode safe) and now word-wraps (white-space: pre-wrap; overflow-wrap: break-word), so long lines no longer force a horizontal scrollbar. - Source import now normalizes filenames and frontmatter (
docs/adr/0004). At import, every source page filename is rewritten to a Unicode-kebab name (Han preserved, whole name incl.md< 32 code points;src/utils/kebab-source-name.ts) and a flat provenance frontmatter is injected/merged —original_filename(the true name, always) plusconverter: minerufor converted files.titleis not injected (a body H1 stays the document title via core's resolution); nooriginal_sha256or timestamps reach the frontmatter, so the bytes stay deterministic for core'spackage_sha256dedup (normalizeForImport+src/utils/frontmatter-merge.ts, which never clobbers author keys). The MinerU sidecar frontmatter changed from the nestedsource: { converter, original_filename, original_sha256 }block (which the reader rendered as raw JSON) to flat keys, and the<stem>-<sha12>/converted-page directory is now<kebab-stem>/<kebab-stem>.md. MinerU uploads under the kebab stem + the original extension (kept for format detection); two inputs whose stems kebab to the same root surface a visibleduplicate_pathskip, and distinct plain names that collapse to the same kebab get a-2/-3suffix. Removed the now-unusedsrc/utils/shorten-filename.ts.
- Tasks toolbar "Task running" status pill. The green-dot live indicator next
to the maintenance buttons (Ingest / Synth / Lint Propose / Lint Apply) was
visually abrupt and redundant — the buttons already disable while any task is
running/pending, which is the real safety gate and is unchanged. Only the pill
(and its now-orphaned
actions.runningcopy +.task-actions__livestyle) was removed; the shared.live-dotclass stays (RetrievePage still uses it).
.env.agent.local→.env.local. The file feeds both the chat agent (server/agent/config.ts) and the/web/*browser helpers (server/web/config.ts), so theagent-specific name no longer fit. Both loaders now read.env.local;.env.agent.exampleis renamed to.env.example. Rename your local file:mv .env.agent.local .env.local.MinerUAPIKey(and itsDIKW_AGENT_MINERU_API_KEYalias) →DIKW_WEB_MINERU_API_KEY. The mineru conversion key is a/web/*concern, so it now follows the standardDIKW_WEB_*prefix, parallel to theDIKW_AGENT_*keys. This is a hard rename with no backward-compatible fallback — update the variable name in your.env.local. A missing key still degrades ImportPage to.md/.pdfonly; the503 mineru_disabledmessage now names the new variable..env.exampledocumentsDIKW_WEB_MINERU_API_KEY(previously the template never listed the mineru key at all), anddocker-compose.ymlnow forwards it into the container alongside the optional Tavily / Jina keys, so the documented deployment env actually reaches/web/mineru/*.
- Long chats now compact their history instead of growing the prompt
unbounded.
AdkAgentRunnerattaches ADK's built-inTokenBasedContextCompactor+LlmSummarizerto theLlmAgent(factory inserver/agent/contextCompactor.ts, reusing the agent's ownMiniMaxLlmfor summarization). When the threshold is crossed, ADK summarizes the oldest events into a persistedCompactedEventandContentRequestProcessorrebuilds the prompt as[summary, ...recent raw events], so the prompt actually shrinks; the summary persists to sqlite and carries across turns. - On by default, env-tunable (
.env.agent.local):DIKW_AGENT_COMPACTION_ENABLED(defaulttrue),DIKW_AGENT_CONTEXT_WINDOW(default1048576, the MiniMax-M3 window),DIKW_AGENT_COMPACTION_RATIO(default0.5), andDIKW_AGENT_COMPACTION_RETENTION(default8). The trigger isround(contextWindow * ratio)= 524,288 tokens at the defaults. Because ADK'sshouldCompactsums per-event prompt-token counts, effective compaction fires somewhat before the live prompt literally reaches the ratio. - The summary never leaks into the chat UI.
AdkSessionStore.projectMessagesfiltersisCompactedEventso the[Previous Context Summary]event is not rendered as an assistant bubble, andmapAdkEventemits no live wire event for it. A summarization failure is swallowed (logged) and the turn proceeds with the un-compacted history. Seedocs/agent.md#context-compaction.
- The IndexedDB convert cache (
dikw-mineru-cache) now prunes entries older than 7 days. Each cached conversion already recorded acachedAt; on every ImportPage mount,IDBConvertCache.sweepExpired(src/utils/mineru-convert.ts) walks the store with a readwrite cursor and deletes any entry whosecachedAtis more thanCACHE_TTL_MS(7 days) old, plus legacy/corrupt rows with a missing or non-finitecachedAt. The TTL is absolute — a cache hit does not refreshcachedAt. The sweep is deferred torequestIdleCallback(with asetTimeoutfallback) so its readwrite transaction never queues ahead of the import flow's foreground cache reads, and runs only at the single point the cache is opened, so no ImportPage / wire-format changes were needed.
- The chat sidecar's agent runtime moved from
@earendil-works/pi-agent-core/@earendil-works/pi-aito Google ADK (@google/adk). The same-origin/agent/*HTTP API and theAgentStreamEventNDJSON wire format are unchanged, so the browser / chat UI is unaffected.AdkAgentRunner(server/agent/adkRunner.ts) drives one turn on ADK'sRunnerand maps each ADKEvent→AgentStreamEvent(mapAdkEvent); the DIKW tool set is now ADKFunctionTools (server/agent/adkTools.ts) reusing the existing core / web tool clients. - The LLM is MiniMax via its Anthropic-compatible endpoint through a custom
MiniMaxLlm extends BaseLlmadapter (server/agent/minimaxLlm.ts, using@anthropic-ai/sdkas transport), and the model is upgradedMiniMax-M2.7→MiniMax-M3(.env.agent.example). MiniMaxthinkingcontent blocks are dropped — onlytext/tool_usecross the boundary. - Sessions now persist to local SQLite (
.agent-sessions/agent.sqlite, appNamedikw-web, userIddemo) via ADK'sDatabaseSessionServiceinstead of one JSON file per session.AdkSessionStore(server/agent/adkSessionStore.ts) projects ADK events into the existingAgentSessionDTO at read time, mirroring title / createdAt / message-count / proposal-status intosession.state. Old.agent-sessions/*.jsonfiles are not migrated (local demo data; expect a one-time reset).
Added: hidden #trace page (OpenTelemetry span waterfall)
- New hidden route
#trace(src/pages/TracePage.tsx) — reachable by URL only, intentionally absent from the sidebar nav (hiddenViewIdsinsrc/App.tsx). It shows a per-session conversation alongside an OpenTelemetry span waterfall served atGET /agent/sessions/{id}/traces. - Spans are captured by a custom
DikwSpanProcessor(registered once per process via ADK'smaybeSetOtelProviders) into an in-memory, boundedSpanStore(server/agent/spanStore.ts), which re-assembles flat span rows into the per-sessionSessionTraceView. Spans are ephemeral — lost on a sidecar restart by design; only the conversation content is persisted (sqlite).
build:servernow bundles with esbuild--packages=external— ADK + MikroORM + native sqlite3 can't be bundled (dynamic driverimport()+ native addons), sodist-server/standalone.mjsimports its dependencies fromnode_modulesat runtime.- The Docker image is rebuilt around
node:24-slim(Debian glibc, for reliable sqlite3 N-API prebuilts) with a dedicatedprod-depsstage (npm ci --omit=dev) whose prunednode_modulesships into the runtime image alongsidepackage.json. TheHEALTHCHECKprobes vianode -e fetch(...)sincenode:24-slimships neitherwgetnorcurl. package.jsongainedoverrides(node-gyp,tar) to clear HIGH npm-audit CVEs and an explicitundicidependency.
- A slow MinerU conversion no longer dies behind a reverse-proxy / tunnel
request timeout (#60). The
convertingpre-stage used to run the whole MinerU pipeline (submit → upload → poll-until-done → download → tar+gzip) inside a singlePOST /web/mineru/convertrequest and wrote no response bytes until it finished, so its time-to-first-byte equalled the full conversion time. Behind a proxy that caps request duration (Cloudflare free ~100s, nginxproxy_read_timeout60s) any conversion slower than that limit had its connection cut mid-flight — the browser saw a transport-levelFailed to fetchand the job was aborted, so larger / scanned PDFs (the slowvlmpath) failed while small files in the same batch succeeded. POST /web/mineru/convertnow returns202 { jobId }immediately and runs the conversion detached in an in-memoryJobStore(server/web/jobStore.ts). The browser polls the shortGET /web/mineru/jobs/<id>for status and fetches the tar.gz fromGET /web/mineru/jobs/<id>/resulton completion (served idempotently within the job's TTL, so a transfer cut mid-flight by a flaky proxy is retry-safe rather than a lost conversion), withPOST /web/mineru/jobs/<id>/cancelto abort — mirroring the task model already used for ingest / synth / lint. Every request is now seconds-short, so none approaches a proxy timeout, and a failure surfaces as a structuredmineru_*code (e.g.mineru_timeout) instead of an opaqueFailed to fetch.- The change is encapsulated in
convertSource(src/utils/mineru-convert.ts); the per-file Import UI gains apollingsubstage while the detached job runs.convertingstays non-resumable across a page reload (the in-memory job store is also dropped on a sidecar restart) — unchanged from before.
- The sidebar logo text and browser tab title are now configurable at
runtime. They default to
OpenDIKWbut can be overridden — without rebuilding — by aconfig.jsonserved from the static root ({ "brand": { "name": { "en": …, "zh-CN": … } } }), fetched once during app bootstrap. A missing or malformed file falls back to the defaults, so existing deployments are unaffected.nameis per-locale and the tab title follows the resolved brand name; the logo image and favicon stay fixed. Seepublic/config.example.jsonand the README "Branding" section. - The top-bar breadcrumb root is now a fixed
Workbench/工作台label instead of the hard-coded brand name, decoupling the "workbench home" crumb from branding. - The agent system prompt is brand-neutral ("a helpful knowledge base
agent") so white-labeled deployments don't surface the
OpenDIKWname in chat.
- Import no longer aborts on a transient gateway/network blip during task
follow (#56). When
dikw-coresits behind a reverse proxy / tunnel (Cloudflare, nginx, Caddy) that occasionally drops or 5xx's a long-lived connection, a single failed poll during a multi-minute stage (typicallysynthwith a slow LLM) used to mark the whole import "failed" even though the task succeeded server-side.DikwClient.streamTaskEventsnow silently reconnects on an upstream 5xx (502/503/504) or a network-levelfetcherror, resuming from the unchangedfrom_seqcursor with capped exponential backoff (1s→15s, up to 8 retries); cancellation and non-transient 4xx errors still propagate at once. If retries are exhausted,ImportPage'sconsumeTaskextends the existinggetTaskFinalEventreconciliation to a thrown poll, so a stage that finished during the outage landsdoneinstead of a spurious failure. The same resilience benefits the Tasks and Wisdom follow paths. - Wisdom Overview metric is vertically aligned. The Wisdom card had no
caption row, so its number sat ~23px below the others in the metric strip.
It now carries a
"wisdom items"caption like every other card, restoring a shared baseline.
- Directory upload removed. The Import picker no longer offers a "Choose folder" button, and dropped folders are ignored with a hint — only individual files are accepted (the file input still takes multiple files at once).
- Unsupported formats are filtered at selection. Files whose extension can't be imported are dropped as soon as they're picked or dropped and reported in a short notice, instead of cluttering the bundle preview's skipped column (which now surfaces only content-level issues like empty bodies).
- Long MinerU filenames are shortened before conversion. MinerU errors on
very long names, so mineru-bound files are uploaded under a ≤25-char stem
(Unicode preserved, extension kept, bytes unchanged → dedup unaffected). The
true original filename is forwarded via a new
originalFilenamequery on/web/mineru/convertso the converted page's frontmatteroriginal_filenamestays complete.
- Sidebar Settings stays reachable. The left sidebar is now pinned to the
viewport (
position: sticky; height: 100vh; overflow-y: auto) so its Settings footer no longer scrolls out of view when a page's main content is taller than the window. The mobile (≤900px) horizontal sidebar is unchanged. - Chat normalizes legacy
wiki/sources. Sessions persisted before dikw-core's 0.4.0wiki/→knowledge/rename still carrywiki/source paths and awikilayer; the right-rail Sources list now displays both asknowledge//knowledge(paths via a newnormalizeKnowledgePathhelper) and dedups a page recorded under both prefixes into a single row. Live core already returnsknowledge/, so new sessions are unaffected, and a frozen assistant message body is left untouched. - Tasks Op filter drops the dead
distillhint. The Op input placeholder no longer suggestsdistill(removed from core) and gains a<datalist>of the current ops —ingest,synth,lint.propose,lint.apply— so they can be searched/selected; free-text entry (e.g.wisdom.write) still works.
- Dropped the dead
distillmaintenance action from the agent: the proposal tool no longer offers it and/v1/distill(now404on core, superseded by/v1/lint/propose) is no longer routed.
- The
#tasksfilter-bar gains Ingest / Synth / Lint Propose / Lint Apply buttons to the right of the Status / Op controls. Ingest, Synth, and Lint Propose start their respective core tasks with default params; Lint Apply runs against the currently-selected succeededlint.proposetask and applies all proposals (pick:null, no review gate) — it is disabled unless such a task is selected. - After firing, the page refreshes the list, selects the new task, and follows
its live event stream (reusing the existing
follow()machinery). - A short background poll of
/v1/tasks(running, thenpending) detects when core is busy — authoritatively, regardless of the active Status/Op filter — and disables the four fire buttons while any such task exists. The gate starts closed on mount and opens only once the first probe confirms core is idle, so it can't be bypassed during the initial network window. A "Task running" indicator marks a real reason (a detected task or an in-flight submit). The detail-panel Stop button changes from a client-only event-stream detach to a realPOST /v1/tasks/{id}/cancel, then re-probes the gate authoritatively — the fire buttons re-enable only when no running/pending task remains (a queued task keeps the gate closed). Follow / Load events still only stream events. A running task hidden by an active Status/Op filter can't be selected to Stop until the filter is cleared — it otherwise releases the gate when it finishes on its own. TasksPagebecomes a second write surface alongside#import(docs updated inCLAUDE.mdanddocs/core-contract.md).
- The
convertingstage no longer looks frozen. Three issues fixed: (1) the<Loader2 className="spin">icon never animated because.spinhad no rule instyles.css(the spin keyframe was only wired to chat-page classes); (2) the singlefetchcovering upload + the multi-minute mineru server conversion + download was labelled "uploading to mineru" the whole time, while the intendedpolling/ "waiting on mineru" substage was never emitted; (3) no elapsed timer or progress bar, so a long wait showed no motion. ConversionProgressnow fixes.spin(reusespr4-spin), shows a per-row live elapsed timer (reusesformatElapsed+ a 1s tick likePipelineSteps), shows an indeterminate progress bar on active rows (reuses@keyframes import-indet), relabels the in-flight wait "Converting on mineru…" with a "can take a minute or two" hint, respectsprefers-reduced-motion, and styles the rows into a proper panel.ConversionFileStategains an in-memorystartedAt. The mineru conversion is a black box (no progress stream), so progress is honestly indeterminate rather than a fake percentage.
consumeTasknow reconciles against the authoritative task row when the event stream drains without atype:"final"event.streamTaskEventsstops polling whentask_statusgoes terminal +has_more:false, but the import pipeline read success from a capturedfinalevent — two signals that can disagree within a single/eventsresponse (livetask_statusvs. a laggingevents[]tail). The race surfaced as "Import failed / ingest failed" while the Tasks page (which readsGET /v1/tasks/{id}) showed the very same task succeeded. NewDikwClient.getTaskFinalEventreads that authoritative row and synthesizes the terminal verdict, so a succeeded ingest/synth/lint task is no longer misreported as a failure. Mirrors WisdomPage's existing drain-then-read-result pattern.
- Contract rename across
src/types.tsand every consumer to match dikw-core 0.4.0 (refactor!: rename K-layer "wiki" to "knowledge"):Layeris now"source" | "knowledge" | "wisdom";InfoResponse.wiki_root→base_root;LayerCounts.wiki_pages→knowledge_pages;StorageCounts.last_wiki_log_ts→last_knowledge_log_ts;ApplyReport.wiki_paths_changed→knowledge_paths_changed. No dual-name compatibility — core rebuilds on incompatibility and keeps no shims. - Dead fields dropped that 0.3.0/0.4.0 stopped returning:
StorageCounts.wisdom_by_status(wisdom is a first-class document layer now, with no candidate status) andLlmInfo.max_tokens_query/max_tokens_distill. Overview's stale "N candidates" detail on the Wisdom card is removed with it. PageReadResult.frontmatter(new in 0.4.0, server-parsed YAML) is surfaced read-only in the Base reader's Info tab, replacing the prior client-side body parse as the metadata source.- Base page now shows only
source+knowledge; wisdom is reachable exclusively via the dedicated#wisdompage. - The
#wikiroute is gone — the Base page lives at#base(matching the "Base" / "知识库" label and the/v1/base/*endpoint family). Legacy#wikino longer resolves and falls back to#overview(no redirect). The in-page[[wikilink]]syntax is unchanged. - The sidecar agent
list_pagestool'slayerenum isknowledge(notwiki).
#wisdomis now backed by the live core HTTP API. List comes fromGET /v1/base/pages?layer=wisdom&active=true(viauseAsyncResource), body + frontmatter fromGET /v1/base/pages/{path}, backlinks fromGET /v1/base/pages/{path}/links?direction=in. Saves go through the asyncPOST /v1/base/wisdomtask: the page POSTs the submit, then drainsclient.streamTaskEvents(taskId)until terminal and unwraps theWisdomWriteReportviaclient.getTaskResult. The mock data modulesrc/pages/__mock__/wisdom-data.tsis deleted along with all of its imports.- Pending draft create flow: the New dialog now constructs a
client-only
__pending__/{slug}draft (no path in core yet). It appears in the tree marked(unsaved), lives in Edit mode, and the first Save is what POSTs to core. Empty bodies are rejected client- side before the request goes out (min_length=1is enforced by core). - Favorite optimistic + rollback: ☆ flips the chip immediately and
POSTs the full page with
no_embed=true(core lacks a dedicated PATCH/status endpoint). The optimistic update rolls back if the task fails.preStarStatusis kept on a frontenduseRefmap — not persisted, refresh resets it (acceptable for now). - Resume on mount: the in-flight write task_id is stored in
sessionStorage["dikw-web.wisdomWrite"](per-core scoped). On mount, if a non-terminal task is found, the page re-enters the saving state and continues polling. Lives insrc/state/wisdom-write.tsnext to the existingimport-pipeline.ts. - K / D candidates for the Add wikilink / Add source picker are
lazy-loaded from
GET /v1/base/pages?layer=knowledgeand?layer=sourcewhen the popover opens. Client-side filter keeps the search-as-you-type UX; core has no search endpoint yet — that's the next iteration's lever if D-layer page counts get large. - Tests:
wisdom.test.tsxrewritten to mockclient.get / post / streamTaskEvents / getTaskResult, covering 14 scenarios (list, detail fetch, pending draft, save round-trip, picker dedup, sources attach, favorite optimistic + rollback, three dirty-edit confirm paths, resume from sessionStorage, empty-body rejection).src/test/mockClient.tsgains agetTaskResultvi.fn stub.tests/e2e/mockApi.tsreturns[]forlayer=wisdom/knowledge/sourceso chrome e2e specs still pass. - Out of scope (next iteration): real status dropdown (draft → published / archived), delete / rename / move, K/D remote search, Playwright spec specifically for the Wisdom write round-trip.
#wisdomis now a fully interactive mock page driven by hardcoded fixtures insrc/pages/__mock__/wisdom-data.ts— no/v1/wisdom*requests at all (the endpoint was retired). Three-pane layout: left directory tree (folder-first, alphabetical), middle read/edit tabs, right rail with Linked references + Sources (sources only shown in Edit; Read merges backlinks inline). Reuses the WikiPagebuildWikiTreepattern andMarkdownViewfor the Read tab.- Read tab runs
injectInlineRefsfromsrc/utils/source-inline-refs.tsto splice[[title|literal]]wikilinks into the body at first occurrence of any backlink title; unmatched backlinks fall into a bottom "Linked references" panel — same behavior as source-layer pages. Wikilink clicks resolve against the wisdom multimap; W→W resolves silently, K→K shows a 2.4s amber toast ("[[title]] isn't a wisdom page in this mock.") via a new.wisdom-toastblock instyles.css. - Edit tab toolbar: "Add wikilink" popover merges K + W candidates
(deduped by path, per-item layer chips for k/w/d) into one picker
matching the "Add source" chip column; cursor-position-aware
[[title]]insertion. New dialog enforces kebab-case slug and author withnewError.authorcopy; lowercased path dedup against existing pages. - Lifecycle: status is
draft | published | favorite | archivedwith a sidepreStarStatusfield so toggling the star ☆ round-trips throughfavoritewithout losing the prior state (favorite → ☆ back topublished/draft/archived, not alwayspublished). - Header layout: timestamp → ☆ favorite → status pill stacked below
the path on the same
.reader-header--stacked+.reader-header__meta--inlinemodifiers WikiPage now uses, so Base reader and Wisdom share chrome. - Saving is a 800ms
setTimeoutthat mutates the in-memory mock map (no API). The timer lives in a ref so navigation/create cancels the in-flight save instead of racing; popover closes on saving=true; Esc handler is one effect with priority unsaved > newDialog > popover. - Dead-code cleanup: removed
WisdomItem/WisdomKind/WisdomStatustypes, thewisdomItemsFixturefromsrc/test/fixtures.tsandtests/e2e/fixtures.ts, and the/v1/wisdommock route fromsrc/App.test.tsxandtests/e2e/mockApi.ts. - Tests: 12 component tests in
src/pages/wisdom.test.tsxcovering tree → read → edit → save, "Add wikilink" K/W picker, "Add source" D picker, dirty-edit confirm + form preservation, and the draft → favorite → draft lifecycle round-trip. e2e:i18n.spec.ts+navigation.spec.tsswitched to the new chrome (Filter wisdom pageslabel,Starred onlychip,exact: trueheading match). - Out of scope: real
GET /v1/base/pages?layer=WISDOM/POST /v1/base/wisdomwiring, async task polling,unresolved_wikilinksround-trip, asset upload, status mutations beyond ☆. Next iteration plugs the mock data source into real core endpoints once visual/interaction sign-off lands.
ChatPagewas concatenatingstreamingSourcesontoactiveSession.sourceswithout crossing the dedup boundary, so when a turn-N stream emitted a source already committed by an earlier turn, React rendered the same composite key (${kind}-${path}-${title}) twice and logged "Encountered two children with the same key". Surfaced under the auto-scroll stress fixture (24 sources × 2 turns → 24 duplicate-key warnings).- Fix folds
streamingSourcesinto the session-base viamergeSources(the samepath/title/kinddedup function used inside the streaming buffer), so identical sources appear once and order is preserved (session first, then streaming-only newcomers). - Tool-call dedup was already safe via
mergeTools' id-based merge — only sources had the cross-boundary gap.
- Standard CommonMark
syntax now routes through the same asset resolver as Obsidian![[path]]embeds. Previously only the Obsidian variant matchedPageAsset.original_paths; standard references likefell back to markdown-it's default renderer, which emitted a literal<img src="./images/scenes/kitchen.png">that the browser then tried to load from the SPA host (404). The 10 images insources/scenes.mdand similar locales-tagged corpus pages now load from/v1/assets/<sha256>200, with token-hydration when a session token is configured. - Three correctness refinements while we were in the file:
alttext passes throughself.renderInlineAsText(token.children)sorendersalt="bold plain"per CommonMark, not the raw source string.titleattribute is preserved across remote, plain-src, and token-hydrated branches (previously dropped on the two local paths).- Non-ASCII paths resolve correctly: markdown-it normalizeLink
percent-encodes Unicode in standard syntax but
original_pathsstores raw, so the resolver tries the literal src first and falls back todecodeURIComponent(src)at the standard-image call site (scoped to that path only; Obsidian resolution stays verbatim).
- Behavior changes worth calling out:
- Empty
![]()short-circuits to nothing instead of rendering the default<img src="">— keeps drafts quiet. - Local refs in surfaces that don't pass
assets(e.g. ChatPage) now render an explicit.md-broken-imageplaceholder where they previously emitted an<img>the browser silently 404'd; the explicit failure is intentional. - Remote URLs (
http(s)://,data:) keep themarkdown-imageclass for consistent CSS; authenticated-hydration stays inert on them because the selector requiresdata-asset-src.
- Empty
- ImportPage now accepts
.pdf / .doc / .docx / .ppt / .pptx / .xls / .xlsxalongside the existing.md+ assets surface. New files run through aconvertingpre-stage that POSTs to a new sidecar route, which calls mineru.net's v4 batch API and streams the converted markdown + assets back as a USTAR tar.gz. Once converted, files join the existing bundle →/v1/import→ ingest → synth → lint pipeline unchanged. The/v1/importwire shape is unmodified, so core needs no changes. - New sidecar namespace
/web/*for dikw-web's own browser helpers, parallel to/agent/*(Pi Agent chat) and/v1/*(dikw-core). First occupants:POST /web/mineru/convert?inputSha=<hex>(single-file multipart in, tar.gz out) andGET /web/mineru/health. Same Node process as the agent sidecar — mounted in dev viawebApiPlugin()invite.config.tsand in prod via the same/webbranch indist-server/standalone.mjs. The browser only talks same-origin. - New optional env var
MinerUAPIKey(aliasDIKW_AGENT_MINERU_API_KEY) in.env.agent.local. The variable name matches thedikw-plugins/.envconvention so the same key file can be reused. Missing key →/web/mineru/*returns503 mineru_disabled, ImportPage shows aMineru not configurednotice, and the file pickeracceptcollapses back to.md/.pdf(PDF still works as a passive asset when referenced from a sibling.md). - Idempotency is the headline contract: same input bytes →
identical
package_sha256, every run. Three layers enforce this: (a) minerucache_tolerance=31536000+data_id=<sha256[:32]>so the server-side cache returns the same conversion for one year; (b) browser IndexedDB cache keyed by SHA-256 of the input file bytes (LRU, 500 MB ceiling,mineruVersion: 1cache-bust knob); (c) byte-stable tar packaging — entries sorted,mtime=0,uid=gid=0, mode0644, frontmatter contains onlyconverter / original_filename / original_sha256(no timestamps, nobatch_id). - New
convertingpipeline stage with aConversionProgressUI surface (src/pages/import/ConversionProgress.tsx). Per-file rows surface substagesqueued / hashing / uploading / polling / downloading / done / failedwith a per-file Skip on failure. Two-concurrent worker pool — conservative on mineru quota while still pipelining well. Refresh duringconvertingis non-resumable in v1 — the pipeline returns to idle, but the mineru server cache + browser IDB cache make re-conversion typically millisecond-fast for the same input bytes. Resume during the existing core-side stages (ingest / synth / lint-*) is unchanged. - Image references in the converted markdown are rewritten from the
mineru
form to project-conventional![[assets/<rel>|alt]]wikilinks, matching what other dikw-web sources do. Resolution uses a 4-tier match (exact / case-insensitive / basename-unique / basename-folded-unique), mirroring thedikw-plugins/dikw-converter-mineruPython implementation. - Token redaction: the mineru bearer token never appears in error
messages or logs in full; only
…<last 4 chars>is ever surfaced. - New files of interest:
server/web/{config,http,mineruClient, mineruConvert,vitePlugin}.ts,src/utils/{tar,tar-reader, mineru-convert}.ts,src/pages/import/ConversionProgress.tsx,tests/e2e/import-mineru.spec.ts.src/utils/tar.tsis a new isomorphic extraction of the USTAR writer that was previously inlined inimport-bundle.ts; the writer's behavior is unchanged, it is now just importable from both the browser bundler and the sidecar's tar.gz response builder.
- The IndexedDB conversion cache stores
cachedAtbut does not yet enforce the planned 500 MB LRU ceiling — it relies on the browser's own quota eviction. Tracking as a follow-up so this PR stays scoped.
End-to-end mineru verification is not part of CI because it requires a
real MinerUAPIKey and burns mineru quota. The e2e suite mocks the
/web/mineru/* wire. Before relying on the feature in a workspace:
- Copy
MinerUAPIKey=…fromdikw-plugins/.env(or wherever you keep it) intodikw-web/.env.agent.local. npm.cmd run devand openhttp://127.0.0.1:4321/#import.- Drop a small PDF or
.docx. WatchConversionProgresswalk throughhashing → uploading → polling → downloading → done, then watch the regular Bundle preview render with the synthesized markdown. - Drop the same file again. The IDB cache should make it instant — no
network call to
mineru.net(verify in DevTools Network). - Compare
package_sha256across two end-to-end runs of the same input — they must be identical for core's dedup to work.
- Split the sidebar's single flat list into three semantic clusters separated
by hairline
border-topdividers:Overview / Import / Base / Graph / Wisdom, thenRetrieve / Chat, thenTasks. Settings stays in the footer group as before. The visibleKNOWLEDGEandSYSTEMuppercase group labels are gone; each<nav>keepsaria-labelfor screen readers. - Rename the en wiki-route concept from
KnowledgetoBaseto match the/v1/base/*core endpoint family. Touches sidebar (nav.wiki), breadcrumb, wiki page heading (pages.wiki.title), the refresh button (pages.wiki.refresh), the Graph detail panel button (pages.graph.openInWiki→Open in Base), and the Graph canvas aria-label (Base graph). zh-CN keeps知识库/在知识库打开unchanged. - zh-CN: rename
nav.wisdom智慧 → 认知 (EnglishWisdomunchanged) and cascade the wisdom page strings (认知沉淀,刷新认知条目, etc.). The Work nav group's zh-CN aria-label is工作to avoid colliding with the contained任务button. - CSS: drop
.nav-group-labelrules; extend.nav-main + .nav-mainto render the hairline divider. Mobile breakpoint (≤720px) flips the divider toborder-leftso the new vertical hairline doesn't appear as a stray top border in horizontal-scroll mode. - Docs:
CLAUDE.md,README.md, anddocs/graph-view.mdupdated to the new Base terminology.
OverviewPage: defensive optional chain ondata?.status.wisdom_by_status?.candidate. Older / 0-wisdom core payloads omit thewisdom_by_statussubkey, which previously threwTypeErrorand blanked the entire app via React 19's tree unmount.
- Slim the 1018-line
ImportPage.tsxinto a 538-line orchestrator plus sevensrc/pages/import/subviews (IdlePicker,BundlePreview,PipelineSteps,LintReview,DoneSummary, plusformat.ts+readDroppedItems.ts). All styling reuses the existingsrc/styles.csstoken system — no UI framework introduced. IdlePicker: dropzone now accepts both files and folders.webkitGetAsEntrywalks the directory tree and injectswebkitRelativePathsocomputeProjectRelPathproduces the same archive paths as the folder picker.BundlePreview: two-column Included / Skipped layout with per-row type icons, byte sizes, and ref counts; skipped rows carry a reason tag.PipelineSteps: resumed pipelines show a blue "Resumed your import" banner above the stepper; a 1-second interval drives the elapsed-time text so it no longer freezes on silent stages. 5-step stepper renders progress bars and an active-stage description card.DoneSummary: large success banner withOpen in Wiki/View graphCTAs (hash navigation), two stat cards (what was added / lint outcome), and a restart tail.- Tests:
ImportPage.test.tsxgrows from 4 to 17 cases across five describe blocks (idle picker, pipeline resume, lint review, done summary, failure & cancel).
handlePipelineError(err, owner)now compares the throwing controller againstcontrollerRef.currentso a lateAbortErrorfrom a cancelled pipeline cannot clobber the state of a freshly-started one after the user clicks "Start a new import".applyLintresetswasResumed, breaking the "refresh into lint-review → click Apply → lint-apply incorrectly renders the resume banner" chain.- Two hardcoded English strings now route through i18n:
stepMetareturned"5 committed"andBundlePreviewrendered"refs 3"even in the Chinese locale. Both have properenandzh-CNtranslations now. - Resume path seeds
pipelineStartedAt = Date.now()so the stepper's elapsed segment is populated on the most important code path (mid-stage refresh). Combined with the 1s ticker, the clock no longer freezes when a task runs silent for 90+ seconds. IdlePicker.onDropis no longerasync.readDroppedItemsrejections (permissions errors, browser quirks) now route throughonDropErrorinto the existingbundleErrorNotice instead of vanishing into the React synthetic-event system as unhandled promise rejections.
- 新增 Import 路由(
#import),sidebar 加同名入口。这是 web 第一个写入 surface:用户在浏览器内选本地文件或文件夹,自动跑「打包 → ingest → synth → lint(propose + 审阅 + apply)」全管线。任意阶段可取消,任务阶段刷新可 续轮询。core 端零改动,沿用/v1/import现有 multipart 协议。 - 浏览器侧打包(
src/utils/import-bundle.ts):手写 USTAR tar + 原生CompressionStream('gzip')+crypto.subtle.digest算 SHA-256。无新增 npm 依赖。package_sha256(md_sha, asset_shas) = sha256(sorted([md, ...assets]).join("\n").encode("ascii"))严格复现dikw-core/src/dikw_core/md_inspect.py:60-66。 - Markdown 引用解析(
src/utils/md-asset-refs.ts):正则与 core 的_IMG_MD/_IMG_WIKILINK一致,sibling-of-md → project-root 两段式 解析。远程 URL 不上传,缺失 asset 在 pre-flight 阻止导入。 - 管线状态(
src/state/import-pipeline.ts):PipelineStage联合 + sessionStorage 持久化(键dikw-web.importPipeline,scope 与serverUrl/token一致; state 携带coreUrl防止跨核重放)。任务阶段(ingest/synth/lint-*)落地 对应task_id,刷新后从streamTaskEvents续跟。上传阶段单次 POST, 刷新即丢,picker 重置。 - Lint 走法:propose 后弹审阅面板让用户勾选要 apply 的 proposal;部分
修复也算完成 —— 只要 task 终态
SUCCEEDED,即便ApplyReport.skipped非空,管线进 done,跳过项以 reason 形式展示,不判 failure。 DikwClient扩展:postMultipart+importBundle+startIngest/startSynth/startLintPropose/startLintApply+getTaskResult+cancelTask。multipart 上传不注入Content-Type,让浏览器自填 boundary。- i18n:
nav.import+pages.import.*全量中英文案。 - 测试覆盖:
import-bundle(Python-golden hashes、USTAR 头结构、gzip magic、 端到端管线包)、md-asset-refs(19 个 case)、import-pipeline(状态机 迁移 + 持久化 + cancel)、DikwClient(4 个新方法 wire)、ImportPage(各 stage 渲染 + cancel)、e2e(sidebar 入口 + picker 预览)。
- Source 层 read tab 渲染时,把已有反向边的 K 页 title(
backlinks ∪ derived) 在 source body 中首次字面出现位置自动合成[[title|原文本]]wikilink, 阅读体验向 wiki 页对齐。未匹配上的 K 页留在底部 Linked references panel。 Source tab(raw view)始终用原始 body,不做替换。 - 匹配规则:大小写不敏感、英文要求
\b边界、CJK 无边界、最小长度英文 ≥3 CJK ≥2、longest-match-first、保留 source 原文字面写法。 - 受保护区段不替换:YAML frontmatter / fenced & indented code(含 mermaid)/ inline code / inline & display math / raw HTML 块(details/table/...)/ existing wikilink(含 image embed)/ markdown link 整体。
- 实现:新增
src/utils/source-inline-refs.ts纯函数(30+ 单元测试), WikiPage 加enhancedSourceBodyuseMemo 串起来,MarkdownView 和 wikilink rule 不动 — click 走现有previewDoc通道直达右侧 preview。 零新增 CSS / 零新增 i18n key。 findPageForTarget加 K 优先(wiki/wisdom > source)— 同名 K 与 source 共存时,合成的 inline wikilink 始终命中 K 页(否则可能 self-route 回 当前 source)。手写[[xxx]]行为保持兼容。- Cache-lag 防御:
injectInlineRefs调用前先按pages.data路径表过滤 refs。resolveDerivedPages用 wire title 占位的 K 页(还没进pages.data)不内联,留在底部 panel,由openBacklink的 path-based fallback 兜底 —— 否则 inline 按钮的openWikiLink会查不到 path → dead link。 - 受保护区段补全(round-2 review):
- CRLF normalize:
injectInlineRefs起手把\r\n归一为\n, frontmatter / fenced / indented / display math 这几个行向 recognizer 才能正确识别 Windows-edited source 文件。 - Fenced code 行扫描:替换原 regex 实现,识别 0-3 空格缩进、
backtick/tilde长度 ≥3、closing fence 长度 ≥ opener、EOF unclosed fence 整段保护。 - Markdown link 兼容:URL 允许一层 balanced parens
(
[docs](https://example.com/path(v2)));新增 reference-style ([text][label]/[text][])和 link reference definition ([label]: url)整段保护。
- CRLF normalize:
- Heading slug 对齐(PR #37 /code-review finding):
slugifyHeading在 lowercase 前先剥掉[[label|literal]]/[[label]]语法, 这样 MarkdownView 的heading_open(看 enhanced body)和extractHeadingsWithSlugs(看 original body)即使输入不同也能 产出同一 slug。修复 source 页 outline 在"heading 含被内联的 K 页 title"场景下getElementById命中 null → 跳转静默失败的回归。
- Source reader merges body
[[wikilink]]backlinks and frontmattersources:provenance into a singleLinked referencespanel withlinked/sourcedlabels. ConsumesGET /v1/base/pages/{path}/provenance?direction=infromdikw-core 0.2.6+; pre-0.2.6cores 404 (or 405) and the panel degrades silently to/links-only behavior. Other/provenancefailures (5xx, network, parse) also clear the sourced channel but log aconsole.warnso the disappearance is debuggable. A dev-modeconsole.warnalso fires when the core contract's layer-safety invariant is violated (non-emptyderived_fromon a source page). - Source labels carry
aria-label(Linked via body wikilink/Linked via frontmatter source) so screen readers announce the evidence channel, not just the chip text.
MarkdownViewno longer wipes mermaid SVG (or chart / hydrated image output) on unrelated parent re-renders. React diffsdangerouslySetInnerHTMLby the wrapping object's identity, not by__htmlstring equality, so a fresh{ __html: html }literal on every render was forcinginnerHTMLto be re-set even when the body HTML had not changed — discarding any post-render DOM mutations. The wrapper is now memoized onhtml. This was latent before but surfaced when the new source-reader effect started firing a secondsetStateafter page load.resolveDerivedPagesno longer silently drops a/provenanceentry when the cachedpageslist lags behind core — it falls back to the wiretitleso a just-synthesized K-page is visible in the source reader without a manual refresh. The click handler closes the loop:openBacklinknow previews-by-path when the entry is not yet inpages.data, so the cache-lag fallback never ships a dead button.previewDocand the newpreviewByPathshare the same request lifecycle. The preview'sOpen as main documentbutton is also hidden for cache-lag stub previews — the selection effect would otherwise round-trip the unknown path back to the default page, silently dropping the click.mergeSourceReferencesis now pure-functional (no in-place mutation of map entries) and sortssourced-only abovelinked- only inside the single-evidence tier so the two evidence channels read as contiguous visual blocks instead of interleaving by title.- Switching between source pages clears the prior page's
Linked referencescontent synchronously, so the panel never shows stale chips during the body-fetch window. Existing race guards in the merge memo remain as defense in depth.
VERSIONfile (4-digitMAJOR.MINOR.PATCH.MICROfrom[0.0.1.0]). The format was gstack/ship-specific and forcedpackage.json.versioninto non-SemVer territory (0.0.1.0), which kneecaps any tool that validates SemVer (npm version <bump>,semver.coerce(), future Renovate / semantic-release).
package.json.versionreverted to standard 3-digit SemVer (0.0.2); now the single source of truth for the project's version.package-lock.jsonre-synced.CLAUDE.mddelivery workflow step 7 reverted togh pr createas the primary PR-creation path./shipis not used in this repo — its 4-digitVERSIONrequirement is incompatible with valid SemVer inpackage.json, and the individual sub-skills it orchestrates (/code-review,/review,code-simplifiersubagent) can be invoked directly when useful. Bumpingpackage.json.versionand writing the matchingCHANGELOG.mdentry are now manual steps in the workflow.CLAUDE.mddelivery workflow step 8 rewritten to emphasize active monitoring of CI status AND PR review comments after pushing, with resolve-as-found discipline rather than batching at merge time. Lists the threegh apiendpoints (reviews / inline comments / top-level issue comments) plus thegh run view --log-failedpattern for failing CI logs.
CLAUDE.md"Working principles" section (Think before coding / Simplicity first / Surgical changes / Goal-driven execution) anchored against the project's/v1/base/graph,src/styles.csstoken system, and#chatcanonical route.CLAUDE.md"Delivery workflow" — 8-step end-to-end loop from request clarification through TDD, codex-review rounds (max 3),/simplify//code-review xhigh, Chrome MCP verification, doc walk (CLAUDE.md+README.md+docs/*.md), PR creation, and squash-merge with explicitgh api .../reviews+/commentspull.CLAUDE.md"Chat / agent rules" subsection covering right-rail session-scoped context, core-first tool preference, and the maintenance-action confirmation gate.VERSION+CHANGELOG.mdscaffolding so the/shipdelivery workflow can run end-to-end on this repo (bump version, write changelog entry, commit, push, open PR) instead of falling back togh pr create.
package.jsonversionsynced to the four-digitVERSION(0.0.1.0); the package is"private": trueand not published, so the extra digit has no registry impact.CLAUDE.mdarchitecture notes corrected against the actual code: Graph page removedwiki/source/allscope toggle (onlysearch+hide-orphansremain),npm.cmd run buildline updated to mentiondist-server/standalone.mjs,dist-server/added to the local/generated list.CLAUDE.mddelivery workflow step 7 dropped the "this repo doesn't carry/shipscaffolding" caveat now thatVERSION+CHANGELOG.mdare in place; prefers/shipover plaingh pr create.CLAUDE.mddelivery workflow step 4 replaced the non-existent/simplifyand unsupported/code-review xhighargument with the actually-installed/code-reviewplugin (five parallel Sonnet agents with confidence scoring) and thecode-simplifiersubagent for cleanup passes.README.md"Where canonical docs live" entry rewritten to point atCLAUDE.mdas the operational guide for Claude Code sessions, and thenpm.cmd run buildrow in the command table updated to matchCLAUDE.mdby mentioningbuild:serverdist-server/standalone.mjs(addresses CodeRabbit feedback on PR #33).
README.md"Where canonical docs live" list extended with adocs/adr/entry so future Architecture Decision Records show up in the doc index.
AGENTS.md— superseded byCLAUDE.mdnow that Claude Code is the sole development surface; unique guidance (sidecarcoreUrlrejection, token-never-displayed rule,.env.agent.localcontainment, generated-artifact ignore list) was integrated intoCLAUDE.mdrather than left in a parallel file.