npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aiquants/enrichment-react

v0.6.1

Published

React UI components for storage metadata AI enrichment: ingest console, pipeline flow viewer, document change history inspector and recent changes card, search and model-comparison workbenches, strategy board, and status, health, storage, and ontology car

Readme

@aiquants/enrichment-react

React UI components for storage metadata AI enrichment: the ingest console, the pipeline flow viewer, the document change history inspector and the recent changes card, the search and model-comparison workbenches, the strategy board, and the status, health, storage, and ontology cards.

Prerequisites

  • Node.js >= 18.0.0
  • React >= 19.0.0 and React DOM >= 19.0.0 (peer dependencies)
  • @aiquants/console-ui (peer dependency — the console root and its stylesheet, the unmeasured label, Measured (every count drawn as a value, durations in ms included, and every value slot that was not measured in the components that write the root's figures), RowButton, the count rule (formatCount), the instant and text-width formats, and the contrast computation and its AAA bars come from it)
  • @aiquants/enrichment-core (peer dependency — every pipeline step, flow, gate, record shape, and search bound the components draw comes from it)

Installation

pnpm add @aiquants/enrichment-react @aiquants/enrichment-core @aiquants/console-ui

Design Rules

  • Host owns routes and lists. No component names an application route or ships a domain list; the host passes onSearch, onCompare, domainOptions, and the like.

  • DOM hooks are data-* / role / element only. Class names are styling and change freely.

  • Rendered inside the console root. InfrastructureHealthMonitor, PipelineFlowViewer, DocumentHistoryInspector, DocumentChangeFeed, ProcessingObjectBrowser, QdrantStorageCard, SyncStatusMonitor, DocumentIngestConsole, the search tab's DegradedSearchBanner, MetadataSearchWorkbench and ModelComparisonWorkbench, and the instant slots RecordedInstant and FinishedInstant read @aiquants/console-ui's ConsoleUiRoot and throw outside it. The other components do not read the root and render anywhere. Every component that reads the root checks it before drawing anything (the root requirement below), so none of them reads the root, draws the root's label or writes in the root's zone under a root it was not written for. This package ships no stylesheet (the host's Tailwind build compiles its classes), and the host loads @aiquants/console-ui/styles/console-ui.css once, with its cui cascade layer ranked above the host's reset, as @aiquants/console-ui's README describes under "Loading the stylesheet" (for a Tailwind v4 host, @import it in the CSS entry after the layer statement @layer theme, base, components, utilities;). Without it the root's --cui-* token variables are unset, a Measured count loses its tabular figures, QdrantStorageCard's name buttons (@aiquants/console-ui's RowButton) are drawn as the browser's plain buttons without the selected look, InfrastructureHealthMonitor's probe-state marks and QdrantStorageCard's health badge lose their role colours (success-fg / danger-fg and their tints), and the root's live region (role="status") is drawn in the flow of the page instead of being visually hidden. The package publishes only dist, and Tailwind v4's automatic source detection skips node_modules, so a Tailwind v4 host also adds @source "../node_modules/@aiquants/enrichment-react/dist/**/*.js"; to its CSS entry (the path is relative to that file; inside this monorepo, point @source at packages/enrichment-react/src/**/*.{ts,tsx} instead). Without that line the host generates none of the components' Tailwind utilities (layout, spacing, colours, borders), so none of the colour pairs under Colour Contrast is drawn. A host without Tailwind is not supported. The root must be locale="ja" and formatLocale="ja-JP": the package's own text is Japanese and its instants are written in ja-JP (RecordedInstant), so under any other root the root's words and digit grouping would sit inside Japanese sentences beside ja-JP times. Every component that reads the root throws RangeError, naming the root's locale and formatLocale, before drawing anything under any other root.

    <ConsoleUiRoot locale="ja" formatLocale="ja-JP" timeZone={displayTimeZone} testId="console">
      <InfrastructureHealthMonitor health={health} />
      <QdrantStorageCard overview={qdrant} />
    </ConsoleUiRoot>
  • Unmeasured is not zero. A figure the source could not measure renders as the root's unmeasured label (未測定 under locale="ja"), never as 0. In InfrastructureHealthMonitor, PipelineFlowViewer, DocumentHistoryInspector, and QdrantStorageCard, every value slot that holds one is @aiquants/console-ui's Measured (directly or through MeasuredText / MeasuredFigure), so data-measured="false" marks every unmeasured value slot there. A run's step report in the history inspector is the one place an unmeasured figure has no slot of its own: a report field that was not measured is named in the step's 未測定: line instead (see DocumentHistoryInspector), and only a page pair with one half measured keeps both Measured slots. The other components keep their own words for a missing value: DocumentIngestConsole writes a missing revision as N/A (it reads the root for its instants and its byte format only), MetadataSearchWorkbench a document absent from one of the ranked lists as ベクトルの列に無い / 語句の列に無い / グラフの列に無い and an empty column as 無し, and OntologyStatusCard a missing count as 未設定; SyncStatusMonitor writes its counts as bare numbers. A count drawn as a value — the Qdrant totals, segment count, collection count, each collection's points and each named vector's dimensions (次元), the health probe's counted details, every probe latency in ms (応答時間 in both InfrastructureHealthMonitor and QdrantStorageCard), the flow metric's sample, succeeded, failed and skipped counts, and every count the core record types as a measured count (the step duration and the report's duration_ms in ms, characters, pages, chunks, points, graph nodes, and the flow's p50 / p95 latencies in ms) — is Measured, written in the root's formatLocale with tabular figures and data-measured="true". The search workbenches write their durations inside a sentence, grouped by the root's count rule (12,345 ms): MetadataSearchWorkbench's search time in the results card's description and ModelComparisonWorkbench's execution time in each column heading; a duration that is not a non-negative safe integer throws RangeError naming it. A measured value that is not a count is written by its own format inside one of the package's two private value slots, in tabular figures with data-measured="true", and when it was not measured the slot writes the same Measured with the label. A value that reads as one unit — a byte size (1.0 MiB), an instant, a probe's port or boolean — is MeasuredFigure, which never breaks inside (white-space: nowrap), so a number never parts from its unit or a date from its time; a text — a probe's text, a service's version, the report's prompt_version and its other texts — is MeasuredText, which wraps with its container. Every recorded instant, in each of the components that write one (InfrastructureHealthMonitor, PipelineFlowViewer, DocumentHistoryInspector, DocumentChangeFeed, SyncStatusMonitor, DocumentIngestConsole), is written by the exported RecordedInstant (or FinishedInstant, for a finish time that may be open) into a MeasuredFigure slot under a -value hook: health-checked-at-value, flow-metric-last-started-at-value, run-history-started-at-value-${id}, run-started-at-value, run-finished-at-value, failed-item-time-value-${id}, ingest-last-run-finished-at-value-${id} and ingest-created-at-value-${id}, so a column of instants lines up digit by digit. A state shown in place of an instant is a word, not a slot: 未完了 for a run that has not finished (FinishedInstant writes it in run-finished-at and ingest-last-run-finished-at-${id}) and 記録なし for a failure the record holds no time for (failed-item-time-${id}). A text with nothing to read is refused like a broken count, with a RangeError that names the slot's hook: drawn, it would be an empty slot that claims a measurement. Pass null for a value that was not measured. Nothing to read means empty or made only of white space, and white space is the union of JavaScript's and Python's \s — 30 code points, among them U+3000 (the ideographic space), U+00A0, U+FEFF, U+001C–U+001F and U+0085 (the slots test each character against [^\s\x1c-\x1f\x85\ufeff], which finds the same characters in JavaScript and in Python's re). The core record schemas still accept such a text, so the refusal is a backstop: a host refuses the value where it reads it, with the same pattern. So every value slot of those components, count or not, keeps one -value hook in both states (data-measured="true" or "false"): a test or a host that reads a value finds it whether or not it was measured. The word inside a note that names what was not measured (the report's 未測定: <fields> line, the flow's empty-metric line and its note, each marked data-unmeasured-note) is the root's labels.unmeasured, and only there. That label is the only spelling of a figure's unmeasured state: the sources write the word only in the gate-state and flag-state vocabulary (門: 未測定, the gate state 未測定 of the flow detail panel, フラグ未測定), each written directly in the element that itself carries data-gate-state="unmeasured" or data-gate="unmeasured", which names a gate whose feature flag is absent — a state, not a figure — and stays Japanese with the rest of the copy. The package's tests hold these rules over the sources and over every drawn state of every component; each test module's docstring describes the gate it keeps.

    An unknown capability renders as 不明, never as 無効.

  • One presentation of a run. Run and step statuses, skip reasons, and triggers are written only through the exported presentation module (pipelinePresentation.ts), so the same status reads the same word in the same colour everywhere.

  • Time zone is the root's. No component takes a timeZone of its own. Every component that prints a recorded instant writes it with RecordedInstant, which reads the root's timeZone (an IANA name, e.g. Asia/Tokyo, that ConsoleUiRoot validates and canonicalises) on every render and formats it in ja-JP to the second (2026/09/24 01:02:03) through @aiquants/console-ui's formatInstant, the one instant format of every console built on that package; the host resolves the zone once on the server and passes it to the root, so the server-rendered page and the hydrated one print the same string, and a host that switches the root's zone re-writes every mounted instant in the new clock. A timestamp is ISO 8601 with an explicit offset (Z, ±hh:mm or ±hhmm) naming an existing date and time: every text the core record schemas refuse (z.string().datetime({ offset: true })) throws, and so does an accepted text that names no instant the console can show (an offset beyond ±23:59, or a wall clock outside years 1–9999 in the zone). The refusal is a ZonedTimeError (a RangeError with reason: "malformed").

  • Layout. The components use Tailwind's stock spacing, sizes, and palette, not @aiquants/console-ui's tokens (its 8-px grid and 11 / 13 / 16 / 20 / 25 px type scale). Most cards are rounded-xl border border-slate-200 border-solid bg-white p-6 shadow-sm; InfrastructureHealthMonitor is rounded-lg border border-solid bg-card p-5 shadow-sm (the host's card theme colours), and its probe-state marks alone take @aiquants/console-ui's colour roles (success-fg on success-tint, danger-fg on danger-tint, see its section). PipelineFlowViewer and the history inspector's step timeline set type at 11 / 13 / 16 px; the history screens (DocumentHistoryInspector, DocumentChangeFeed) and the search tab (DegradedSearchBanner, MetadataSearchWorkbench, ModelComparisonWorkbench) take their type, spacing and colours from @aiquants/console-ui's tokens and roles only; the other Tailwind-styled components also use 10 px, text-xs (12 px), and text-sm (14 px). QdrantStorageCard and SyncStatusMonitor are styled inline, with no Tailwind classes. The two cards have an 8-px radius and 16-px padding, set type from 11 px up to 18 px (QdrantStorageCard) or 20 px (SyncStatusMonitor), 12 and 14 px included, and use spacings such as 2, 6, 10, and 14 px that are off the 4-px half-step grid.

  • One card-title scale. Every card title (h2) is 16 px bold (text-base font-bold, or fontSize: "16px", fontWeight: 700 where the component styles inline) — the same scale @aiquants/enrichment-react-router and @aiquants/enrichment-governance-react use, so sibling cards on one tab read as one heading level. tests/HeadingOutline.spec.tsx checks every panel's h2.

  • Borders name their style. Every Tailwind border-width utility (border, border-t, border-2, …) sits in the same class list as border-solid (or another explicit style), and every divide-x / divide-y next to divide-solid. A host whose base layer sets border-style: none on every element (for example * { @apply border-none }) would otherwise leave those utilities with a width and no line. tests/borderStyleContract.spec.ts scans every string literal in src/ and fails on a width without its style. Per-side widths (border-t border-solid) rely on the host's Tailwind preflight setting the other sides to width 0.

  • Nothing is clipped without a way to read it, and nothing that reads as one unit breaks inside where the layout holds it. Labels that must stay on one line (whitespace-nowrap) move to the next row instead of shrinking (the health badges, the Qdrant card's badge, latency and refresh button), and unbroken identifiers (URIs, versions, endpoints, the run inspector's report texts) wrap anywhere (wrap-anywhere) inside their card instead of overflowing it or ending in an ellipsis. A value that reads as one unit — a count with its unit, a latency, a byte size, an instant, the unmeasured label — never breaks inside in these places, so 1,234 件 never stands as 1,23 over 4 件 and 1,234 ms never as 1,234 over ms there: InfrastructureHealthMonitor's rows (in a narrow row the label wraps beside the value instead), the Qdrant card's header (the value moves to the next line whole) and its named-vector tags, the measured figure in every MeasuredFigure slot (byte sizes, instants, a probe's port or boolean), PipelineFlowViewer's p50 / p95 latencies, and the history inspector's step report (each report value sits below its label with the column's whole width). Any other Measured slot (a step's duration in its summary line, the flow metric's counts) does not hold the rule itself, and neither does an unmeasured MeasuredFigure slot elsewhere (the Qdrant card's RAM and disk tiles, the flow metric's last start), which is @aiquants/console-ui's Measured with its label: Measured joins a count and its unit with a plain space and leaves its text free to wrap, so in a column narrower than the value a number can part from its unit and the label can break between its characters.

Exported Components

1. DegradedSearchBanner

The note a degraded search answer carries: a warning Notice under the host's testId whose summary (${testId}-summary) names the strategy asked for, the one that answered and the answer's quality (縮退して答えています: ハイブリッド を求め、キーワード で答えました (品質: 縮退 (ベクトルもグラフも走らず))。), or says that no tier ran when not even the lexical tier did (… 密ベクトル を求めましたが、走った層はありません …: the answered strategy is derived from the tiers outside SQL, so it reads KEYWORD then), followed by one sentence per reason in the order the answer recorded them (${testId}-reasons, items ${testId}-reason-${index} with data-reason carrying the code). Props: metadata (is_degraded, requested_mode, search_mode, executed_tiers, quality_tier and degradation_reasons of a DocumentSearchMetadata) and testId. A search that was not degraded draws nothing, so a strategy the user chose (KEYWORD) is not a degradation; a degraded answer without a reason draws the summary only.

Each reason is written by degradationReasonText(code) through DEGRADATION_REASON_LABELS, one sentence per code of the core's closed SEARCH_DEGRADATION_REASONS (a total map, so a code added to the vocabulary without wording fails to compile). A code the vocabulary does not know (a newer API) is shown as it is, never dropped, and a code that arrives twice is listed twice: a skipped tier is never hidden.

2. DocumentIngestConsole

Manual ingestion form (URI, confidentiality level, target domain, labels), file inspection result, projection capability per level (有効 / 無効 / 設定が渡されていないため不明, data-state="true|false|unknown"), and the recent-ingest table. The table shows sync status, the last pipeline run (ingest-last-run-${id}: the shared run badge ingest-last-run-status-${id} with data-presentation, the failed step, and the finish time ingest-last-run-finished-at-${id}, its instant under ingest-last-run-finished-at-value-${id} or 未完了 while the run is open), the registration time (ingest-created-at-value-${id} inside ingest-created-at-${id}), both in the root's zone, and — when onShowHistory is given — a 履歴 button per row (ingest-history-button-${id}) that asks the host to open the document's whole processing history (called for rows without a recorded run too; the inspector then says there is none).

Required props: tenantId, sources: readonly SourceSpec[] and domainOptions: readonly { key; label }[] (empty → the select is disabled and ingest-domain-empty explains why). The console reads the root's zone and format, so it renders inside ConsoleUiRoot. sources is the deployment's source catalog (GET /ai/sources, @aiquants/enrichment-core's SourceCatalogSchema), and the console names no scheme of its own: one button per source in the catalog's order (scheme-select-${scheme}, labelled <scheme>:// (<title.ja>); the first source is selected at first, and again when the selected one leaves the catalog), the prefix badge (scheme-prefix-badge), the input's example (input_hint.ja) and guidance (guidance.ja), the check of the identifier written after the prefix (identifierMatches: a full match of the source's form, refused as <title>の識別子の形ではありません (例: <input_example>)。). The console reads a typed or pasted text in this order: a full URI of a catalog source is split by its longest prefix (sourceOfUri) and kept as written, a text that fully matches the chosen source's form is that source's identifier, and only then is an identifier extracted from a pasted text such as a share link (pastedIdentifier), so a path that merely contains a share link's marks stays with its own source. A URI no source's prefix starts names every prefix of the catalog in its refusal, and .. stays refused as the console's own guard. An empty catalog shows 取り込める源が配備にありません。 (ingest-no-sources, a warning Notice) with no button and a disabled input, and nothing can be inspected or sent. recentItems is readonly RecentIngestEntry[]: a readable row { kind: "readable", item: RecentIngestItem } or a restricted mark (RestrictedIngestMark, V11) that the table draws as one row with the fixed text 隔離 (RESTRICTED) の文書, its registration time and its sync status only (recent-item-restricted-${position}, data-restricted-mark="ingest"), with no history entry and no row action; a mark whose syncStatus is null (the document has no enrichment record) reads the neutral 取り込みの記録なし (NO_ENRICHMENT_RECORD_TEXT, no status badge) in its status cell (recent-item-restricted-status-${position}). Each RecentIngestItem carries lastRun: ReadableEntry<{ runId; status; retryScheduled; failedStepKey; finishedAt }> | RestrictedRunMark | null; a FAILED run with retryScheduled: true reads 失敗 (再試行予定), and a last run that is a mark reads 隔離されていた期間の実行 with its status (ingest-last-run-restricted-${id}). Each RecentIngestItem also carries unlock: { protectionScheme; disposition; copyReason } | null, how the revision lost its password (null for a revision without one): the row writes the protection removed and how the unlocked file became the original, with the copy's reason (ingest-unlock-${id}, data-disposition, data-protection, the names being @aiquants/enrichment-core's labels). When onPromoteUnlockedCopy is given, a copy that is the original because the operator could not write the source (isPromotable) offers 源を写しで上書き (ingest-promote-button-${id}) on its current revision; a copy of a source never overwritten (a mail, another application's object, a host file) offers nothing. isBusy says that one of the console's submissions (an inspection, an ingestion, a retry or a promotion) awaits its answer: the host carries them on one fetcher, which a second submission would replace, so every submit control is disabled meanwhile. The row promotingId names reads 源を写しで上書き中... (aria-busy), and the button's accessible name holds its visible words (<file name> の源を写しで上書き). A promotion requested from the console (a click, not a focus) is announced once the row records it (copy_promoted), and focus moves to the row's unlock record only when it fell to the document body, never from another control; the promote, retry and history buttons are 44 px tall. A run that failed at unlock_document adds the hint that a file password matching the file must be added before a retry, and that a protection no component removes must be removed where the file was made (ingest-unlock-hint-${id}). listRefusal?: string is the full sentence that refuses the operator the list (no file:read, or a column-scoped one; V6 / V9): when set, the list is replaced by that sentence in a warning Notice (recent-items-refused), with no row, no empty sentence (取り込み履歴はありません。 would claim that nothing was ingested) and no row action. onShowAllObjects?: () => void draws 全ての処理オブジェクトを見る (recent-items-all-objects, @aiquants/console-ui's Link, an in-document move) beside the list's heading and calls it when pressed; it is not drawn while the list is refused (the full list refuses the same operator). The table here is a window of the recent rows; the full list is ProcessingObjectBrowser. The status badges read their words from SYNC_STATUS_LABELS (syncStatusPresentation.ts), the table ProcessingObjectBrowser reads too. The inspection result writes the file's size under inspection-file-size-value (MeasuredFigure: tabular figures, data-measured="true", never broken between the number and its unit, the same slot as QdrantStorageCard's byte sizes) with the package's one byte format: binary units (B below 1 KiB, then KiB / MiB / GiB / TiB) chosen after rounding (1,048,575 bytes is 1.0 MiB), the mantissa grouped in the root's format locale (3,072.00 TiB). fileSize must be a non-negative safe integer; otherwise the console throws RangeError naming fileSize, with @aiquants/console-ui's formatCount refusal as its cause, instead of writing a negative size as 0 B or a fraction without a unit. That refusal is a backstop: thrown while the console renders, it would take the operator's unsent form input with the console. The host refuses a broken size where it reads the inspect response (its schema fails the inspect request, shown as a failed inspection), so a value that reaches the console is valid. A missing revision is written N/A, not the root's unmeasured label.

3. EnrichmentStrategyBoard

Per-domain strategy rows with each stage's enablement laid over the tenant feature flags.

4. InfrastructureHealthMonitor

Connectivity probes for Qdrant, Neo4j, Redis, SQL Server, and the backend API. Required prop: health (the check time is written in the root's zone under health-checked-at-value, inside health-checked-at). Text details (health-detail-${service}-${key}), the versions the probes read, and the endpoint footer (service-endpoint-${service}) wrap inside the card; the endpoint is shown in full. Each service's version sits under one hook, service-version-value-${service} inside service-version-${service}: the version the probe read (data-measured="true"), set in monospace as code, or the root's unmeasured label when it read none (version: null, data-measured="false"), never N/A, set like the unmeasured label of every detail row and not as code. A version that is empty or only white space throws RangeError naming the hook. A host whose probe cannot read a version passes null: a stand-in word such as unknown would be drawn as a version the probe read. Each probe's state reads one word of the console's probe vocabulary (service-status-${service}): 正常接続 or 接続エラー, the same words the overall badge composes (全サービス正常接続 / 一部サービス接続エラー) and QdrantStorageCard writes for the same probe. Every probe-state mark — the overall badge and each probe's badge with its dot (service-status-dot-${service}) — is drawn in @aiquants/console-ui's colour roles on the mark's own surface: success-fg on success-tint for a connected probe and danger-fg on danger-tint for a failed one, the pairs of the CUI's tinted success and danger pills (both in its CONTRAST_PAIRS), the roles QdrantStorageCard's badge takes for the same probe. Each probe's error text (service-error-${service}) is a failure mark in either state, always danger-fg on danger-tint, a connected probe's included. Each graphic (the overall badge's icon, a badge's dot) paints currentColor, the mark's own text colour, and nothing in a mark pulses or fades. The mark carries its surface, so it reads the same on the host's light or dark card (the CUI palette is light-only: its stylesheet declares color-scheme: light on the root). Outside those marks nothing in a service card changes with the probe's state (no element, class or attribute, its frame and fill included); the state is shown by the marks alone. The error text is drawn only when the probe's errorMessage has something to read (the value slots' definition above); it breaks anywhere and is shown in full, like the endpoint. An errorMessage with nothing to read (empty or only white space, as an Error thrown with no message gives) says nothing, so the card draws no box for it and the failure reads from the probe's badge; the card does not throw on it, since a monitor that stopped on a probe's empty error would hide the failure it exists to show. Each probe's latency is a Measured count with ms (health-latency-value-${service}, data-measured="true"), grouped like every other duration of the console (1,234 ms), and must be a non-negative safe integer (otherwise RangeError). Measure it on a monotonic clock and round to whole ms (Math.round(performance.now() - started)): the wall clock (Date.now()) can step back during a probe, and a negative latency stops the card. Every detail's value sits under one hook, health-detail-value-${service}-${key}. A counted detail (collectionsCount, nodeCount, totalKeys, endpointsCount) given as a number is a Measured count with 件 and must be a non-negative safe integer (otherwise RangeError); any other number (a port, not grouped) and a boolean (true / false read 有効 / 無効) are figures, and a number that is not finite (NaN, Infinity) throws RangeError naming its hook; a string, a counted detail given as a string included, is the probe's text (all data-measured="true"). A text detail must not be empty or only white space: it throws RangeError naming its hook (health-detail-value-sqlServer-fullVersion: …), and the whole card with it, so a host passes null for a text it could not read, never a stand-in word (unknown) or an empty string, which would read as a value the probe read. A detail the probe could not read (null), counted or not, is the root's unmeasured label (data-measured="false"), so a Redis whose DBSIZE failed (totalKeys: null) reads as unmeasured, never 0 件. Each row lays out by how its value may break: a count, the latency, the unmeasured label, a port and a boolean's word never break inside, and the label beside them wraps instead; a text (an edition, a URI) and a version the probe read break anywhere inside the card beside a label that stays whole.

5. MetadataSearchWorkbench

The document search of the search tab (the public API's GET /ai/search/documents). Props: tenantId, onSearch(params, signal) and the optional initialParams: Partial<DocumentSearchParams>, which fills the query, the strategy, its controls and the filters. The host owns the route and the transport: onSearch answers a DocumentSearchAnswer, or rejects with an Error whose message the workbench shows as it is (search-failure, announced again when the same failure repeats and removed by the next answer), and it should stop when signal aborts.

  • Query card (search-query-card, with data-mode and data-running): the query (search-query), the strategy (search-mode, a segmented control of the core's SEARCH_MODES worded by SEARCH_MODE_LABELS, HYBRID unless initialParams names another, its description in search-mode-description) and the page size (search-limit). Only the controls the chosen strategy reads are drawn and sent: the similarity floor for DENSE (search-threshold, starting at the midpoint of its bounds, since no floor is the request's own default), the fusion (search-fusion, rrf or linear by SEARCH_FUSION_LABELS) and its k (search-rrf-k) for HYBRID, and the vector weight α (search-alpha) for a linear fusion. The numeric bounds and defaults come from SEARCH_PARAM_LIMITS.
  • Filters behind the 詳しい条件 disclosure (search-filters): the confidentiality level (search-confidentiality, 全ての区分 or one of the core's FILTERABLE_CONFIDENTIALITY_LEVELS worded by CONFIDENTIALITY_LEVEL_LABELS, so RESTRICTED is never offered), the department id (search-department), the MIME type (search-mime-type, a contains match) and the tag (search-tag, an exact match). A filter is sent only when given, trimmed.
  • Developer presets behind the 開発者のプリセット disclosure (search-presets): each of SEARCH_PRESETS (search-preset-${id}-apply) sets the strategy and the page size, and the floor or the fusion only where the preset names them; applying one is announced.
  • Checked before sending: the query (1–500 characters after trimming), the department (empty, or a positive integer) and the text filters (at most 100 characters after trimming). Nothing is sent while a field is unacceptable, and each field says why only once a send has been attempted.
  • One search at a time: the submit button (search-submit) is busy while a search runs and a second submit is not taken. A search the workbench leaves behind (unmounted) is aborted through its signal, and its late answer or failure is not drawn.
  • Results (search-results, described <n> 件 · <ms> ms · 品質: <quality>): the run (search-run: a pill per tier, search-tier-${tier} with data-ran; the answering worker's breakers search-breakers by BREAKER_STATE_LABELS; the row scope search-row-scope; the column scope with the names of the columns it hid, search-column-scope) and a master–detail (336 px master, 544 px detail minimum). The master is the documents (search-hits, rows search-hit-row with data-rank): the rank and the title, and the fused value as a meter (search-hit-${rank}-fused). The first row is selected when an answer arrives, and the number found is announced. The detail (search-hit-detail) shows the selected document's values (search-hit-scores: the fused value as a meter; the vector value by vectorScoreText, a cosine as a percentage of 1 and BM25 or a mixed value as a plain number; the lexical and graph rank values), its fields (search-hit-fields, the update time in the root's zone under search-hit-mtime-value, the tags as pills), its graph relations (search-hit-relations) and its structured data (search-hit-structured, a scrolling code block).
  • Scopes: a column the caller's scope hides reads HIDDEN_FIELD_TEXT (非表示 (列のスコープ)), and a document's relations are withheld while a row scope applies (their targets have not passed the row check).
  • Degraded answers draw DegradedSearchBanner (search-degraded) between the card and the results.
  • Developer inspector behind the 開発者の点検 disclosure (search-inspector): a segmented control (search-inspector-view) of the request as sent, the response, the run trace and the graph trace, drawn in a code block (search-inspector-code). Before any search every view says まだ検索していません。; after one, the request view keeps what was sent even when the controls move. The copy button (search-inspector-copy) writes the shown view to the clipboard and announces it; a copy that fails says why (search-inspector-copy-failure).

A fused value outside [0, 1] (the meter's range) or a search time that is not a non-negative safe integer throws RangeError instead of being drawn.

<MetadataSearchWorkbench tenantId="default" onSearch={(params, signal) => api.searchDocuments(params, signal)} />

6. ModelComparisonWorkbench

Runs one query against two strategy variants (the public API's POST /ai/search/compare-variants) and lays their ranked chunks side by side in a comparison grid (compare-grid: a row per rank, a column per variant, each cell compare-${a|b}-${rank}-${layout}). Props: variants: readonly ComparisonVariant[] and onCompare(input, signal), which answers a VariantComparisonAnswer or rejects with an Error whose message the workbench shows, and should stop when signal aborts. A variant is identified by its tm_enrichment_strategy row (strategyId), never by its variant key: every domain has its own default row. Options and column headings read <label> (<embeddingModel>), where the host passes label as <domain> / <variant>; a heading adds the variant's execution time (変種 A: <name> · 1,234 ms), and a result whose strategy has since left variants is headed 戦略 #<id>.

  • Form (compare-card, with data-running; compare-form): variants A and B (compare-variant-a, compare-variant-b, the first two of variants by default), the query (compare-query, 1–500 characters after trimming) and the page size (compare-limit). It sends { query, strategy_ids: [A, B], limit } with the query trimmed. The same variant on both sides says why under B and sends nothing; fewer than two variants draw an info note (compare-variants-insufficient) instead of the form.
  • Cells: a chunk is identified by <metadata_id>:<chunk_index> (variantHitKey). A cell shows 文書 <id> · チャンク <index> · <score>, the score rounded to SCORE_DISPLAY_DECIMALS (4) decimals, and the chunk's text; past the end of a variant's list it says この順位の結果無し. Scores are never normalized: two variants may embed with different models, so a cosine from one is not comparable to a cosine from the other.
  • Agreement (compare-agreement) is measured on ranks by rankAgreement(a, b): the shared chunks with each variant's count (compare-shared-count), the Jaccard overlap (compare-overlap-ratio), the mean absolute rank difference of the shared chunks (compare-rank-difference) and whether the bodies are shown (compare-text-hidden).
  • What did not run or is hidden: a variant whose dense tier did not run (dense_ran: false) says why in a warning of its own (compare-degraded-a / compare-degraded-b, its reasons by degradationReasonText); a body the caller's scope hides (text_hidden, or a null text) reads 非表示 (列のスコープ) in every cell; when neither variant has a chunk, compare-empty replaces the grid.
  • Failures: an answer without one of the requested strategies (答えに戦略 #<id> の結果がありません。) or an onCompare that rejects (its Error's message, or 比べを実行できませんでした。 for a rejection that is not an Error) shows the compare-failure alert and removes the previous results; a rejection never escapes the component. One comparison runs at a time, and one the workbench leaves behind is aborted through its signal.

An execution time that is not a non-negative safe integer throws RangeError naming the strategy instead of being drawn.

<ModelComparisonWorkbench
  variants={[{ strategyId: 11, label: "GENERAL_DOCUMENT / default", embeddingModel: "text-embedding-3-large" }, { strategyId: 12, label: "GENERAL_DOCUMENT / ruri", embeddingModel: "cl-nagoya/ruri-v3-310m" }]}
  onCompare={(input, signal) => api.compareVariants(input, signal)}
/>

7. OntologyStatusCard

Ontology schema version, lifecycle phase, promotion thresholds, and class / predicate counts. The card does not read ConsoleUiRoot, so its counts are not Measured: the constraint, index and HITL counts are written ungrouped with 件 (未設定 when the source returned null), and each class's count is grouped by the runtime's default locale (toLocaleString()), so a server and a browser with different default locales can group it differently.

8. PipelineFlowViewer

The ingestion pipeline as the flow each document kind actually takes, drawn from PIPELINE_FLOWS / PIPELINE_STEPS:

  • Kind tablist (flow-kind-${kind}) following the ARIA tabs pattern: only the selected tab is in the Tab order; ArrowLeft / ArrowRight (wrapping), Home, and End move focus and select.
  • Figure (flow-svg, role="group" named <kind> のパイプライン, so its step buttons stay in the accessibility tree), drawn at its natural size inside a horizontally scrolling tabpanel (flow-scroll, named <kind> の流れ, described by the visible hint flow-scroll-hint, with edge shadows on the side where the figure continues). The figure is never scaled to fit. Nodes flow-node-${key} are disclosure buttons for the detail panel (role="button" with aria-expanded, and aria-controls naming the panel while it is open): click, Enter, or Space opens the panel on the step, or closes it when it already describes that step; Escape clears. They carry data-gate-state = open / closed-skip / closed-fail / unmeasured, data-gate-policy = none / skip / fail / mixed, data-selected, data-focused; a focused node draws a focus ring (flow-node-focus-ring-${key}, sized by FLOW_FOCUS_RING). The node's gate line (flow-gate-badge-${key}) carries the node's data-gate-state too.
  • Dependencies in text as well as in the drawing. Edges flow-edge-${from}-${to} draw each dependsOn; each node is also described (aria-describedby → its <desc>, flow-node-dependencies-${key}) by what it waits for in the drawn flow — 依存: <labels> in declaration order, or 依存なし (流れの最初に走る) — so a screen reader can tell the text-layer flows' branch after extract_text and join at resolve_body from a chain.
  • Gates as the flow applies them. Every gate is read through the core's flowStepGates(flowStep), so a flow that overrides a step's gate policy (the image flow's ocr_document and the media flow's analyze_media, their only body source) is drawn with the meaning it has there. A node's gate line (flow-gate-badge-${key}, also its accessible name <label> (<line>)) is gateCaption(state, policy): a closed state names its consequence (門: 閉 (省略) / 門: 閉 (失敗)), and an open or unmeasured gated step adds its policy from GATE_POLICY_LABELS (e.g. 門: 開・閉じたら失敗). Nothing else is drawn on a step: the search path's circuit breaker is neither read nor fed by the pipeline.
  • Detail panel (flow-step-detail) with the step's definition, the projection step's chunk source written <words> (<code>) through CHUNK_SOURCE_LABELS (flow-step-chunk-source, e.g. 前段が書いた行をそのまま使う (existing)), the steps it waits for (flow-step-depends-on) and the steps that wait for it (flow-step-dependents), and its gates in the drawn flow (flow-gate-${flag} with data-gate-state and data-when-closed; a failing gate names its failure status, e.g. 閉じたときは FAILED_EXTRACT で失敗), plus flow-gate-policy-override when the flow overrides the step's declaration. When it opens or starts describing another step, the focused element outside it is recorded as its opener: the pressed node (pressing a node — click, Enter, or Space — leaves focus on it), or, in controlled mode, the host control that held focus when selectedStepKey changed. Closing it with its close button returns focus to that opener while it is still in the document; with no opener (opened with nothing focused) or one that has left the document, focus goes to the node of the step it described.
  • Metrics per kind (flow-step-metric, data-kind): only the PipelineStepMetric whose (documentKind, stepKey) is the selected kind and step; the heading and flow-metric-scope state the window (直近の集計 (<kind>・直近 <metricSampleWindow> 件)), and a pair without a metric says so in a sentence headed by the root's unmeasured label (未測定 (<kind> の実行でこの段を記録したものはまだありません), flow-step-metric-empty) instead of showing another kind's figures. The sample, succeeded, failed and skipped counts (flow-metric-sample-count-value, flow-metric-succeeded-value, flow-metric-failed-value, flow-metric-skipped-value) are Measured counts and p50 / p95 (flow-metric-p50-value, flow-metric-p95-value) are Measured milliseconds, all in the root's format locale (1,234 ms); p50 / p95 are taken over measured durations only (flow-metric-note, which names the same label: 所要 未測定). The last start sits under one hook either way (flow-metric-last-started-at-value): the time in the root's zone with data-measured="true", or the root's unmeasured label with data-measured="false"; a p50 / p95 the metric did not measure is likewise the label. metricSampleWindow in the heading and flow-metric-scope is written as a bare number: it is a count inside a sentence, so it is not Measured, not grouped, and does not follow the root's formatLocale.
  • MIME classification table (flow-mime-table) inside the horizontal scroll wrapper flow-mime-scroll (relative overflow-x-auto), so narrow screens scroll it instead of pushing it past the card.

Kind and step selection are each controlled or uncontrolled. Required props: featureFlags, metrics, metricSampleWindow (the number of newest step records per (kind, step) the host aggregated into each metric; a positive integer, otherwise RangeError).

<PipelineFlowViewer featureFlags={flags} metrics={stepMetrics} metricSampleWindow={stepMetricWindow} onSelectStep={(key) => setStep(key)} />

The pure layout lives in layoutFlow(flow, geometry = FLOW_GEOMETRY) (pipelineFlowLayout.ts): rank = longest dependency path; an edge that skips layers gets one virtual slot per skipped layer, the slots take part in barycenter ordering, and the edge runs through them, so no edge enters a node it does not connect. Text is sized by @aiquants/console-ui's fixed sans model (textWidthEm: full-width glyph 1 em, half-width 0.55 em), the one text-width model of the console, of which this package keeps no copy; FLOW_GEOMETRY.node.width is the widest step label at 13 px plus the text inset on both sides (FLOW_NODE_TEXT), rounded up to the 8-px grid with ceil8 (184 for the shipped steps); the padding keeps a focus ring (FLOW_FOCUS_RING) inside the figure and the gaps keep it off the neighbouring nodes.

9. DocumentHistoryInspector

A non-modal dialog panel (history-inspector, role="dialog" without aria-modal) showing one document's change history across its revisions (S1 §3.3.5-13, §8.19). It reads through the host and draws only what the host returns.

Props: host: { loadHistory, compare, readLines } (DocumentHistoryHost without loadChanges; every read resolves to a ResourceOutcome), metadataId (any revision row of the document), target: DocumentHistoryTarget ({ kind: "latest_state" } for an ingest row's history, { kind: "run", runId } for a recent run's detail, { kind: "state", stateId } for a change's arriving state), openRequest: number (changed by the host on every open or re-target from outside, including a press naming what is already shown; each change reads the history again, keeping the shown one until the answer settles and withdrawing it on a 403, and applies target to the fresh history; while the re-read is in flight, a target the shown history does not hold reads 文書の履歴を読み込んでいます… (history-detail-loading, aria-busy) instead of being reported missing), onClose(), returnFocusFallback: () => HTMLElement | null, and the optional captureState(metadataId) (the host's manual capture, a ResourceOutcome<{ outcome }>).

  • Header: h2 (history-heading) 文書の履歴 — <登録名> and a dl with the URI, the current revision (r3), the confidentiality level, the number of revisions and the last change's time.
  • Timeline (master, MasterDetail with a 288 px master and a 448 px detail minimum): one group per revision, newest first; the current revision opens, older ones start closed; inside, the readable runs (with the badges of the state each produced, 本文 +12 −3, チャンク +2 −1 ~4, 分類 変更) and the states no readable run produced (上書きの前の記録, 手動の記録, 記録の起点), newest first, as SelectableList rows (history-item, data-item-key="run:<id>" / "state:<id>"). Marks are written as sentences: 失敗した実行の後の状態, 記録されていません (a finished run with no terminal step or a failed one), この変化には記録されなかった実行 #n の変化も含まれます, and ソースは同じで、モデルの出力だけが変わりました. Restricted marks (V11) sit in their own group ahead of the revisions, 隔離されていた期間の記録 (n 件), closed at first, one row each (history-restricted-mark, data-restricted-mark="state" | "run") with 隔離されていた期間の状態 / 隔離されていた期間の実行, the time and the result only; a mark cannot be selected, pinned or returned to, and its React key is its position in the answer.
  • Detail (history-detail): one h3 (history-detail-heading, the same element whatever it shows) and the selected state's change from its predecessor (DocumentChangeView: a nav of changed facets, one section with an h4 per changed facet, 変化なし for the rest; the text and summary as TextDiffView, the chunks as ChunkChangeTable; the assets facet's added, removed and modified items each in one flat list in the tree order of their keys, the derived tree paths, each row <treePathLabel(path)>: <value>). A first state and a state whose predecessor the operator cannot read both say 直前の記録との比較はありません。 and read no comparison. A run shows its summary, its error, its state's change and its step timeline (PipelineRunSteps, the terminal step named 文書の状態の記録) in a <details>. A fan-out step's child records are grouped under the step in one flat list in the tree order of their targets' tree paths (targetTreePath), each card named <treePathLabel(path)> (#<id>) (添付 0 > 書庫の中身 3 (#412)); the records that carry one path (the same child recorded twice in a run, which the record contract admits) share that path's item in sequence order, so none is dropped and the timeline does not throw; the group's 対象 <n> 件 counts the records. A child record without a tree path (the path comes from a join, so a record written before or during the paths' backfill has none) is drawn, with its card, in the group's one alert (AlertMessage, run-step-group-pathless-<stepKey>), named #<step record id> → #<target id> under the reason 木の道を持たない記録 (移行の途中), and the group opens; each such card paints the console's surface with the fg role itself (data-cui-surface="surface", data-cui-fg="fg"), the pair the placed cards sit on, so its text never sits on the alert's tint; no path is made up for it and no record is dropped. The timeline is grouped once per run, so drawing the same run again keeps the alert and the focus inside it.
  • Nodes of a child tree (TreePathList): both lists above are flat by design: one semantic <ol role="list"> with one <li> per listed node, in the tree's pre-order (compareTreePaths, whatever order the items come in). Each item is named by its path's full label (treePathLabel, which the list hands to the card or row it draws), and the label names every level, so the list nests nothing: a tree of any depth draws at the same DOM depth (React's recursive walk of its components stays as shallow) and adds no width. Only the listed nodes are drawn; no item stands in for an ancestor the list does not hold, so an unchanged ancestor never reads as added or removed. Every node's <li> is <list testId>-item-<path>, scoped by its list, so the same path in two lists (added, removed and modified, or two fan-out groups) never collides. The order and the labels are computed once per items array. The DOM order is the tree's pre-order, so focus moves through the cards and rows in the order they are drawn; the names and the targets are those of the cards and rows themselves. The list paints nothing (no class, no style); the explicit role="list" keeps it a list for WebKit and VoiceOver under a reset of list-style. A path outside the grammar, a path listed twice or a kind with no name throws RangeError. 比較の起点にする pins a state (history-pin-button); the next state selected is compared with it, older side first; 外す (history-unpin) releases it.
  • Text diff: statistics (lines and graphemes, the similarity or the root's unmeasured label), attribute changes (改行 CRLF → LF), the hunk index (45 行目 (p.3) +3 −1), and hunk bodies read only when opened, DIFF_LINE_PAGE_MAX lines a page (LoadMore). Side by side applies from splitViewMinPx (2 × (line-number gutter + sign column + 40 characters) + 16 px in the 13 px mono face, 784 px for 5 digits); narrower, the view keeps one column and says 幅が足りないため 1 列で表示しています in a visible sentence, and the change view announces it once through the root's announcer, however many diffs it holds. It measures its width with ResizeObserver. Each line table is named after its facet and hunk (本文 45 行目 (p.3) +3 −1); side-by-side rows are paired page by page (never across a page boundary) and keyed by their line numbers, so a page read later only appends rows and LoadMore focuses its first row.
  • Failures are worded by labels.failureSentence: a 403 is 文書の履歴を閲覧する権限がありません。<原因>。 and withdraws a history already shown; any other failure is 文書の履歴を読めませんでした (<理由>)。 (比較, 差分の行 for the other reads) with a reload. A document with no record says so in a role="status" sentence.
  • Manual capture: with captureState, 今の状態を記録 appears while the newest readable run has no state; the outcome is announced (記録しました / 前の記録から変化はありません) and the history is read again.
  • Nothing to select: a history with restricted marks only says 選べる記録はありません。 (role="status") in the detail instead of prompting a pick. The revision groups' open state belongs to the document: re-targeting another document starts again from the defaults.
  • Focus: only the h2 and the detail h3 take focus (both tabIndex={-1}); pressing the row already selected moves focus to the h3 too. When 今の状態を記録 disappears while holding focus (the recorded state was read back), focus moves to the detail h3, or the h2 when nothing is selected; when a re-read's 403 withdraws content that held focus, focus moves to the h2. A refresh failure that repeats with the same words is announced again (its AlertMessage occurrence counts the failures). The h2 takes it on mount and whenever openRequest changes, recording the element outside the panel that held focus as the opener; an item picked in the panel's own timeline moves focus to the h3; a load settling moves nothing. A selected item inside a closed revision group opens the group. Pinning and unpinning move focus to the control that replaced the one holding it. Escape (with focus inside) and the close button call onClose; on unmount focus returns to the opener while it is still in the document, otherwise to returnFocusFallback().
<DocumentHistoryInspector
  host={documentHistoryHost} metadataId={entry.metadataId} target={entry.target}
  openRequest={entry.openRequest} onClose={closeHistory}
  returnFocusFallback={() => tabRefs.current[activeTab]} captureState={captureDocumentState}
/>

10. QdrantStorageCard

Qdrant collections, point counts, named vector dimensions, health latency, and resource utilization. Required prop: overview: QdrantStorageOverview, the collections the host measured or the reason it could not, as one value in one of two states: { collections; probe_failure: null } when measured, { collections: []; probe_failure } when not. Omitting it cannot assert a measurement the host did not make, and an empty list never stands for an unreachable store. A reason that is empty or only white space (the value slots' definition above) throws RangeError, since it would draw an empty warning beside unmeasured totals, and so does a reason that comes with collections (the type has no such state; the card refuses one that arrives through JSON instead of dropping either half). Every count on the card is a Measured count in the root's format locale (qdrant-total-points-value, qdrant-total-vectors-value, qdrant-segment-count-value, qdrant-collections-count, each row's collection-points-value-${collection_name}, each named vector's dimensions vector-tag-dense-size-${collection_name} / vector-tag-image-size-${collection_name} with 次元, and the latency qdrant-latency-value with ms, labelled 応答時間 like InfrastructureHealthMonitor's, each with data-measured), so the rows and the totals always group digits alike. The point-count column is end-aligned, header included, so its tabular figures line up by place value; the other columns are start-aligned. While overview.probe_failure is set the totals and the collection count are unmeasured, not 0. RAM and disk sit under one hook each (qdrant-ram-usage-value, qdrant-disk-usage-value), written by the package's one byte format, the same as DocumentIngestConsole's file size, in a slot that never breaks between the number and its unit. A measured size is written in binary units (B below 1 KiB without decimals, so 512 B and 1,023 B, then KiB / MiB / GiB / TiB, powers of 1024), its unit chosen after rounding (1,048,575 bytes is 1.0 MiB, never 1,024.0 KiB), its mantissa grouped in the root's format locale in tabular figures (1,048,524 bytes is 1,023.9 KiB; TiB has no upper bound, so 2^53 − 1 bytes is 8,192.00 TiB) with data-measured="true", and a size that is null or absent is the root's unmeasured label (data-measured="false"). latency_ms follows the same clock rule as InfrastructureHealthMonitor's latencyMs: a monotonic clock, rounded to whole ms. The health badge (qdrant-health-badge) reads the console's probe vocabulary, 正常接続 or 接続エラー, in its colour roles (success-fg on success-tint, danger-fg on danger-tint): the words and the roles InfrastructureHealthMonitor draws for the same probe. In the header the badge, the latency (qdrant-latency) and the refresh button (更新, 更新中… while refreshing) never break inside; the title group takes the width the button leaves and wraps between its items, so in a narrow card the badge and the latency move under the title whole and the button stays at the end of the first line. Every figure follows the console's one validity rule: null or absent means not measured (ram_bytes, disk_bytes and segment_count are typed number | null, like every measured count of @aiquants/enrichment-core), and a number — points_count, vectors_count, a named vector's size, segment_count, ram_bytes, disk_bytes or latency_ms — must be a non-negative safe integer: a negative, fractional or non-finite figure is a broken probe, so the card throws RangeError instead of writing it as a value or as unmeasured. The card asks @aiquants/console-ui's formatCount to judge every figure, keeping no copy of the rule; for a figure it sums, converts or writes per row, the error names the figure in front of formatCount's message (ram_bytes: …, points_count of collection <name>: …, vectors_config.dense.size of collection <name>: …), with that error as its cause, and a broken latency throws formatCount's own error through Measured. Each collection's points_count and vectors_count is checked before it is summed, so a broken row (-1 and 1001) never hides inside a total that looks valid (1,000). The collections table is named by its heading (コレクション一覧 (n 件)) and sits in a scroll frame (qdrant-collections-scroll) that is a region of the same name and takes focus (tabIndex={0}), as @aiquants/console-ui's DataTable frames a table: a keyboard user can scroll a table wider than the card even when it holds no button, and the frame's 4 px inner padding (space-1) keeps the focus ring of a name button at its edge unclipped. The frame itself takes the browser's own focus ring, not the console's focus token: @aiquants/console-ui draws its ring only on the elements it owns, and it publishes no table frame outside DataTable. That exception ends when the card moves to DataTable with selectable rows. A collection name never breaks inside a word, with or without selection, so the name column is equally wide in both, and a named vector's tag (Dense: 1,024 次元) never breaks inside itself, so a number never parts from its unit and 次元 is never split; the tags wrap between one another, and a card too narrow for one tag per line scrolls the table instead of splitting the names or the tags. With selection ({ selectedCollectionName, onSelectCollection, detailId }, one value, so a host cannot pass the selected name without the handler or the detail region), each row's collection name is @aiquants/console-ui's RowButton (collection-select-${collection_name}), the same control SelectableList draws for a master list: data-selected and aria-expanded say whether it is selectedCollectionName, and aria-controls names detailId, the id of the host's region that shows the selected collection (render it while the card is drawn). The button is stretched to the full height of its cell (the cell gives its height to a flex box the button fills), so its start-edge bar, its focus ring and its pointer target cover the whole cell however tall wrapping vector tags make the row. The console stylesheet draws its focus ring (2 px of the focus token, 2 px out: 5.25:1 on the row's surface and 4.82:1 on the selected one in the light scheme, 6.76:1 and 5.60:1 in the dark one) and its selected look: the info-tint surface, which is only 1.09:1 (dark: 1.21:1) against the row's surface, and a 4 px info-accent bar at the start edge (4.82:1 on the tint, dark: 5.60:1), so the selection is not shown by colour alone, and the bar is redrawn in Highlight under forced colours. tests/colorContrast.spec.tsx recomputes these four ratios in both schemes from @aiquants/console-ui's TOKENS with its contrastRatio. The selected row is tinted with the same info-tint token. The name button is the row's one control: Tab reaches it after the frame, Enter, Space or a click selects once, and a click elsewhere on the row selects nothing, so the other cells' text stays selectable. The row stays a table row whose cells keep their column headers, and the name column (header, button text and plain names alike) starts at the button's space-3 inset. Without selection the names are text and every row is data-selected="false".

11. SyncStatusMonitor

sync_status breakdown with individual and bulk retry controls. Required props: counts, totalFiles; failedItems is readonly FailedSyncEntry[], readable rows { kind: "readable", item: FailedSyncItem } and restricted marks (RestrictedFailureMark, V11) drawn as one row with 隔離 (RESTRICTED) の文書, the sync status and the failure time only, without a retry (failed-item-restricted-${position}); listRefusal?: string replaces the list with the refusal sentence in a warning Notice (failed-items-refused), with no row, no empty sentence, no row retry and no count in the list's heading, while the status tiles stay; each failed item's failed_at (an ISO 8601 instant with an explicit offset) is written in the root's zone under failed-item-time-value-${id} inside failed-item-time-${id}, the same clock as every other panel, and null reads 記録なし. The monitor reads the root's zone, so it renders inside ConsoleUiRoot. onShowAllFailures?: () => void draws 全ての失敗を見る (failed-items-all-failures, a Link) below the list's heading and calls it when pressed, unless the list is refused; the list here holds its first rows only.

12. DocumentChangeFeed

The recent document changes (S1 §3.3.5-4), a full-width Card 直近の文書の変化 holding a DataTable read a keyset page at a time (DOCUMENT_CHANGE_FEED_PAGE_SIZE rows, LoadMore). Props: host: { loadChanges(cursor, signal) } and the optional onOpen(metadataId, stateId) (without it there is no 開く column).

  • A row (change-feed-row, data-row-kind) shows the arriving state's time, the document (name and URI), the revisions (r4 → r5 or r4 (同じ版)), the cause, the operator, the changed facets as badges and 開く, which selects the arriving state and names it to assistive technology (開く doc-9.pdf r4 → r5 の変化, the file name and the revisions in VisuallyHidden). No storage figure is shown (V5).
  • A restricted mark (V11) is one row of the same table with its time and 隔離 (RESTRICTED) の文書 only; its React key is its position (changeFeedRowKey).
  • A failed re-read or later page keeps the rows shown and says why (a failed re-read in change-feed-refresh-failed, announced again when the same failure repeats); a later page's 403 re-reads the first page, and a 403 there withdraws the list (文書の履歴を閲覧する権限がありません。<原因>。).

13. ProcessingObjectBrowser

The processing-objects list, a full-width Card 処理オブジェクト一覧 that reaches every td_file_metadata row of the tenant the operator may read (the ingested documents and the nodes of their trees), newest first, read a keyset page at a time (PROCESSING_OBJECT_PAGE_SIZE rows, LoadMore) through @aiquants/enrichment-core's processing-objects contract. Props: host: ProcessingObjectHost ({ loadPage(filter, cursor, signal) }, processingObjectHost.ts), the controlled filter: ProcessingObjectFilter with onFilterChange(filter), and the optional onOpenHistory(metadataId) (without it there is no 履歴 column).

  • The filters: three SegmentedControls (processing-objects-kind / -revision / -deleted), a Select of the sync status (processing-objects-status; a node is filtered by its root's status) and a name query form (processing-objects-query, 絞り込む). Each control asks the host for the whole filter with the one key changed (pressing the pressed segment asks nothing); the query is sent trimmed when the form is submitted, a blank query drops the query, and a query over PROCESSING_OBJECT_QUERY_MAX_CHARS characters is not sent and shows the field's error. The labels are PROCESSING_OBJECT_KIND_LABELS, PROCESSING_OBJECT_REVISION_LABELS, PROCESSING_OBJECT_DELETION_LABELS and PROCESSING_OBJECT_STATUS_LABELS.
  • A changed filter reads the list again from its first page (the paged key is the filter). The first page must carry the total (ProcessingObjectPage.total); a first page without one breaks the host's contract and throws to the nearest error boundary.
  • The count 全 N 件のうち M 件を表示しています。 (processing-objects-count) is written above the table and announced (useAnnounce) whenever it changes.
  • A row (processing-object-row, data-row-kind document / node / restricted) shows the id, the kind (文書 (<document kind>) or the asset type's name), a node's place (treePathLabel and 根 <id>), the name (a document's name and URI, a node's source_name or 名前なし), the classification and revision (CONFIDENTIALITY_LEVEL_LABELS, r<n>, 旧版, 削除済み), the status and the times (登録 / 更新, RecordedInstant). A document's status is its sync status badge (SYNC_STATUS_LABELS, syncStatusTone) or 取り込みの記録なし, and the error of a failure; a node's is 根の実行 with its root's sync status and 読み: <step> <status> (<skip reason>) (nodeReadingText, 読みの記録なし when the root's latest run did not read it). 履歴 opens the document's history (文書の履歴), and a node's opens its root's (根の履歴); the button names its object to assistive technology.
  • A restricted mark is one row with 隔離 (RESTRICTED) の文書, its sync status and its times only, keyed by its position. A list narrowed by name holds no mark (the host's contract).
  • A failed re-read or later page keeps the rows shown and says why (processing-objects-refresh-failed); a later page's 403 re-reads the first page, and a 403 there withdraws the list (処理オブジェクトを閲覧する権限がありません。).

Search Exchange Shapes (documentSearch.ts)

T