@aiquants/enrichment-react 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -42
- package/dist/index.cjs +3 -1
- package/dist/index.d.cts +149 -133
- package/dist/index.d.ts +149 -133
- package/dist/index.js +3 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ React UI components for storage metadata AI enrichment: the ingest console, the
|
|
|
6
6
|
|
|
7
7
|
- Node.js >= 18.0.0
|
|
8
8
|
- React >= 19.0.0 and React DOM >= 19.0.0 (peer dependencies)
|
|
9
|
-
- `@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
|
|
9
|
+
- `@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)
|
|
10
10
|
- `@aiquants/enrichment-core` (peer dependency — every pipeline step, flow, gate, record shape, and search bound the components draw comes from it)
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
@@ -19,7 +19,7 @@ pnpm add @aiquants/enrichment-react @aiquants/enrichment-core @aiquants/console-
|
|
|
19
19
|
|
|
20
20
|
- **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.
|
|
21
21
|
- **DOM hooks are `data-*` / `role` / element only.** Class names are styling and change freely.
|
|
22
|
-
- **Rendered inside the console root.** `InfrastructureHealthMonitor`, `PipelineFlowViewer`, `DocumentHistoryInspector`, `DocumentChangeFeed`, `QdrantStorageCard`, `SyncStatusMonitor`, `DocumentIngestConsole`, 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.
|
|
22
|
+
- **Rendered inside the console root.** `InfrastructureHealthMonitor`, `PipelineFlowViewer`, `DocumentHistoryInspector`, `DocumentChangeFeed`, `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.
|
|
23
23
|
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.
|
|
24
24
|
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;`).
|
|
25
25
|
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.
|
|
@@ -36,10 +36,10 @@ pnpm add @aiquants/enrichment-react @aiquants/enrichment-core @aiquants/console-
|
|
|
36
36
|
|
|
37
37
|
- **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.
|
|
38
38
|
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.
|
|
39
|
-
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
|
|
39
|
+
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.
|
|
40
40
|
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,
|
|
41
41
|
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"`.
|
|
42
|
-
The
|
|
42
|
+
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.
|
|
43
43
|
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.
|
|
44
44
|
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.
|
|
45
45
|
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:
|
|
@@ -58,12 +58,13 @@ pnpm add @aiquants/enrichment-react @aiquants/enrichment-core @aiquants/console-
|
|
|
58
58
|
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.
|
|
59
59
|
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"`).
|
|
60
60
|
- **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).
|
|
61
|
-
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
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
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;
|
|
62
|
+
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).
|
|
63
|
+
`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.
|
|
64
|
+
- **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`.
|
|
64
65
|
- **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.
|
|
65
66
|
`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.
|
|
66
|
-
- **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
|
|
67
|
+
- **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.
|
|
67
68
|
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:
|
|
68
69
|
`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).
|
|
69
70
|
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.
|
|
@@ -72,11 +73,11 @@ pnpm add @aiquants/enrichment-react @aiquants/enrichment-core @aiquants/console-
|
|
|
72
73
|
|
|
73
74
|
### 1. `DegradedSearchBanner`
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
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),
|
|
77
|
+
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).
|
|
78
|
+
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.
|
|
76
79
|
|
|
77
|
-
Each reason
|
|
78
|
-
`remote_tiers_unavailable` (the search service is unreachable or down), `remote_tiers_timed_out` (no answer in time — retry or narrow the query), `remote_tiers_refused_caller` (the service refused the caller — sign in again or check the grant), `remote_tiers_rejected_request` (the service refused the query — revise it) and `remote_tiers_failed` (the service failed while answering — report it if it persists).
|
|
79
|
-
None of them is folded into a generic "could not connect".
|
|
80
|
+
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.
|
|
80
81
|
|
|
81
82
|
### 2. `DocumentIngestConsole`
|
|
82
83
|
|
|
@@ -84,7 +85,20 @@ Manual ingestion form (URI, confidentiality level, target domain, labels), file
|
|
|
84
85
|
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 —
|
|
85
86
|
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).
|
|
86
87
|
|
|
87
|
-
Required props: `tenantId`, `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`.
|
|
88
|
+
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`.
|
|
89
|
+
`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),
|
|
90
|
+
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>)。`) and the extraction of an identifier from a pasted text such as a share link (`pastedIdentifier`, before a full URI is split by its longest prefix, `sourceOfUri`).
|
|
91
|
+
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.
|
|
92
|
+
`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;
|
|
93
|
+
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}`).
|
|
94
|
+
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
|
|
95
|
+
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
|
|
96
|
+
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.
|
|
97
|
+
`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.
|
|
98
|
+
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`),
|
|
99
|
+
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.
|
|
100
|
+
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}`).
|
|
101
|
+
`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.
|
|
88
102
|
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:
|
|
89
103
|
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.
|
|
90
104
|
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.
|
|
@@ -112,28 +126,46 @@ Each row lays out by how its value may break: a count, the latency, the unmeasur
|
|
|
112
126
|
|
|
113
127
|
### 5. `MetadataSearchWorkbench`
|
|
114
128
|
|
|
115
|
-
|
|
116
|
-
The
|
|
129
|
+
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.
|
|
130
|
+
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.
|
|
131
|
+
|
|
132
|
+
- **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`).
|
|
133
|
+
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`.
|
|
134
|
+
- **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.
|
|
135
|
+
- **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.
|
|
136
|
+
- **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.
|
|
137
|
+
- **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.
|
|
138
|
+
- **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).
|
|
139
|
+
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.
|
|
140
|
+
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).
|
|
141
|
+
- **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).
|
|
142
|
+
- **Degraded answers** draw `DegradedSearchBanner` (`search-degraded`) between the card and the results.
|
|
143
|
+
- **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.
|
|
144
|
+
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`).
|
|
145
|
+
|
|
146
|
+
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.
|
|
117
147
|
|
|
118
148
|
```tsx
|
|
119
|
-
<MetadataSearchWorkbench tenantId="default" onSearch={(params) => api.
|
|
149
|
+
<MetadataSearchWorkbench tenantId="default" onSearch={(params, signal) => api.searchDocuments(params, signal)} />
|
|
120
150
|
```
|
|
121
151
|
|
|
122
152
|
### 6. `ModelComparisonWorkbench`
|
|
123
153
|
|
|
124
|
-
Runs one query against two strategy variants and lays their ranked
|
|
125
|
-
Options and column headings read `<label> (<embeddingModel>)`, where the host passes `label` as `<domain> / <variant
|
|
126
|
-
A hit title is cut to the column visually only: the full title is the `title` of `compare-hit-title-${strategyId}-${rank}` and part of the row's accessible name (`<rank> 位 <title> (スコア <score>)`).
|
|
127
|
-
Scores are never normalized (they are different quantities per tier); they are written rounded to `SCORE_DISPLAY_DECIMALS` (4) decimals by `formatComparisonScore(score)` (`compare-hit-score-${strategyId}-${rank}`), and the value as returned stays in the row's `data-score` and the score's tooltip.
|
|
128
|
-
Agreement is measured on ranks by the exported `rankAgreement(a, b)` — Jaccard overlap of hit ids and the mean absolute rank difference of shared ids.
|
|
129
|
-
A failed comparison — `{ ok: false, reason }`, a missing result (`応答に戦略 #<id> の結果がありません。`), or an `onCompare` that rejects (`比較を実行できませんでした: <message>`) — shows the `compare-failure` alert and removes the previous results; a rejection never escapes the component.
|
|
154
|
+
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.
|
|
155
|
+
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>`.
|
|
130
156
|
|
|
131
|
-
|
|
157
|
+
- **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.
|
|
158
|
+
- **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.
|
|
159
|
+
- **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`).
|
|
160
|
+
- **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.
|
|
161
|
+
- **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.
|
|
162
|
+
|
|
163
|
+
An execution time that is not a non-negative safe integer throws `RangeError` naming the strategy instead of being drawn.
|
|
132
164
|
|
|
133
165
|
```tsx
|
|
134
166
|
<ModelComparisonWorkbench
|
|
135
167
|
variants={[{ strategyId: 11, label: "GENERAL_DOCUMENT / default", embeddingModel: "text-embedding-3-large" }, { strategyId: 12, label: "GENERAL_DOCUMENT / ruri", embeddingModel: "cl-nagoya/ruri-v3-310m" }]}
|
|
136
|
-
onCompare={(
|
|
168
|
+
onCompare={(input, signal) => api.compareVariants(input, signal)}
|
|
137
169
|
/>
|
|
138
170
|
```
|
|
139
171
|
|
|
@@ -177,20 +209,30 @@ Text is sized by `@aiquants/console-ui`'s fixed sans model (`textWidthEm`: full-
|
|
|
177
209
|
|
|
178
210
|
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.
|
|
179
211
|
|
|
180
|
-
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;
|
|
212
|
+
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;
|
|
213
|
+
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 }>`).
|
|
181
214
|
|
|
182
215
|
- **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.
|
|
183
216
|
- **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>"`).
|
|
184
217
|
Marks are written as sentences: `失敗した実行の後の状態`, `記録されていません` (a finished run with no terminal step or a failed one), `この変化には記録されなかった実行 #n の変化も含まれます`, and `ソースは同じで、モデルの出力だけが変わりました`.
|
|
185
218
|
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.
|
|
186
|
-
- **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
|
|
219
|
+
- **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>`).
|
|
220
|
+
A first state and a state whose predecessor the operator cannot read both say `直前の記録との比較はありません。` and read no comparison.
|
|
187
221
|
A run shows its summary, its error, its state's change and its step timeline (`PipelineRunSteps`, the terminal step named `文書の状態の記録`) in a `<details>`.
|
|
222
|
+
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.
|
|
223
|
+
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.
|
|
224
|
+
The timeline is grouped once per run, so drawing the same run again keeps the alert and the focus inside it.
|
|
225
|
+
- **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.
|
|
226
|
+
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.
|
|
227
|
+
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`.
|
|
188
228
|
`比較の起点にする` pins a state (`history-pin-button`); the next state selected is compared with it, older side first; `外す` (`history-unpin`) releases it.
|
|
189
|
-
- **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);
|
|
229
|
+
- **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);
|
|
230
|
+
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.
|
|
190
231
|
- **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.
|
|
191
232
|
- **Manual capture:** with `captureState`, `今の状態を記録` appears while the newest readable run has no state; the outcome is announced (`記録しました` / `前の記録から変化はありません`) and the history is read again.
|
|
192
233
|
- **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.
|
|
193
|
-
- **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).
|
|
234
|
+
- **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).
|
|
235
|
+
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()`.
|
|
194
236
|
|
|
195
237
|
```tsx
|
|
196
238
|
<DocumentHistoryInspector
|
|
@@ -219,14 +261,16 @@ The frame itself takes the browser's own focus ring, not the console's `focus` t
|
|
|
219
261
|
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.
|
|
220
262
|
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).
|
|
221
263
|
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.
|
|
222
|
-
The console stylesheet draws its focus ring (2 px of the `focus` token, 2 px out: 5.25:1 on the
|
|
264
|
+
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)
|
|
265
|
+
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`.
|
|
223
266
|
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.
|
|
224
267
|
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.
|
|
225
268
|
Without `selection` the names are text and every row is `data-selected="false"`.
|
|
226
269
|
|
|
227
270
|
### 11. `SyncStatusMonitor`
|
|
228
271
|
|
|
229
|
-
`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}`);
|
|
272
|
+
`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}`);
|
|
273
|
+
`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`.
|
|
230
274
|
|
|
231
275
|
### 12. `DocumentChangeFeed`
|
|
232
276
|
|
|
@@ -236,36 +280,45 @@ The recent document changes (S1 §3.3.5-4), a full-width `Card` `直近の文書
|
|
|
236
280
|
- A restricted mark (V11) is one row of the same table with its time and `隔離 (RESTRICTED) の文書` only; its React key is its position (`changeFeedRowKey`).
|
|
237
281
|
- 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 (`文書の履歴を閲覧する権限がありません。<原因>。`).
|
|
238
282
|
|
|
283
|
+
## Search Exchange Shapes (`documentSearch.ts`)
|
|
284
|
+
|
|
285
|
+
The shapes the two workbenches exchange with their host, field for field the public API's answers, and the words they are drawn with:
|
|
286
|
+
`DocumentSearchParams` (`threshold` is sent with `DENSE` only; `fusion`, `rrf_k` and `alpha` with `HYBRID` only), `DocumentSearchAnswer` (`hits: DocumentSearchHit[]`, `authz_scopes`, `search_metadata: DocumentSearchMetadata`), `DocumentSearchHit` (every column `null` when the caller's scope hides it, `graph_relations` `null` while a row scope applies), `DocumentScores` (`fused`, `vector: DocumentVectorScore | null`, `lexical_rank`, `graph_rank`),
|
|
287
|
+
`VariantComparisonInput` (`query`, `strategy_ids: readonly [number, number]`, `limit`), `VariantComparisonAnswer` (`results: VariantResult[]` in the request's order, `text_hidden`), `VariantResult` (`strategy_id`, `execution_ms`, `dense_ran`, `degradation_reasons`, `hits: VariantHit[]`) and `VariantHit` (`metadata_id`, `chunk_index`, `text`, `score`).
|
|
288
|
+
`degradation_reasons` is `readonly string[]`, not the closed vocabulary, so an answer from a newer API that adds a code is still taken and the code shown as it is.
|
|
289
|
+
The words: `SEARCH_MODE_LABELS`, `SEARCH_FUSION_LABELS`, `QUALITY_TIER_LABELS`, `BREAKER_STATE_LABELS`, `DEGRADATION_REASON_LABELS` / `degradationReasonText`, `HIDDEN_FIELD_TEXT`, and the value helpers `vectorScoreText`, `SCORE_DECIMALS` (3), `rankAgreement` / `RankAgreement` and `variantHitKey`.
|
|
290
|
+
|
|
239
291
|
## Colour Contrast
|
|
240
292
|
|
|
241
|
-
`tests/colorContrast.spec.tsx`
|
|
242
|
-
The
|
|
243
|
-
The package keeps no luminance computation or AA bar of its own, the colour spec included.
|
|
293
|
+
`tests/colorContrast.spec.tsx` checks the parts of the console drawn through `@aiquants/console-ui`'s colour roles against WCAG 2 AAA in both colour schemes: text needs 7:1 (1.4.6) and graphics 3:1 (1.4.11), both read from `@aiquants/console-ui`'s `CONTRAST_MINIMUM`, and the ratio is its `contrastRatio`, the repository's only contrast computation.
|
|
294
|
+
The package keeps no luminance computation or contrast bar of its own, the colour spec included.
|
|
244
295
|
|
|
245
|
-
- **
|
|
246
|
-
- **
|
|
247
|
-
- **Status badges and counts:** the ontology card's HITL count (`ontology-unresolved-count`, `amber-700` on its tile) and the strategy board's stage switches, open (`emerald-700` on `emerald-50`) and closed (`slate-600` on `slate-100`).
|
|
296
|
+
- **History screens:** `DocumentHistoryInspector` and `DocumentChangeFeed` paint their colours only through `@aiquants/console-ui`'s roles; their specs run `scanColourRoles` over the rendered panel, so every text sits on a pair `CONTRAST_PAIRS` lists for both schemes (the diff pieces are `fg` on `danger-tint` / `success-tint`).
|
|
297
|
+
- **Search tab:** `DegradedSearchBanner`, `MetadataSearchWorkbench` and `ModelComparisonWorkbench` paint nothing themselves (no class, no style); their specs run `scanColourRoles` over the console root with an answer drawn (the degraded note, the run, the hit list and the detail; the comparison's agreement, a variant's warning and the grid), so every text sits on a pair `CONTRAST_PAIRS` lists for both schemes.
|
|
248
298
|
- **Probe-state marks:** the health monitor's overall badge, each probe's badge, and each probe's error text (`service-error-${service}`), with every probe connected and with some probes failed (a connected one among them carrying an error text), each at rest and while re-probing, with and without the refresh button; the Qdrant card's health badge (`qdrant-health-badge`), connected and failed, at rest and while refreshing.
|
|
249
|
-
Each mark's texts pass `@aiquants/console-ui/testing`'s role scan `scanColourRoles` as exactly its own pair, its state's `success-fg` on `success-tint` or `danger-fg` on `danger-tint`, and the error text's `danger-fg` on `danger-tint` in either state (pairs `@aiquants/console-ui`'s `CONTRAST_PAIRS` lists, so each meets the text bar).
|
|
299
|
+
Each mark's texts pass `@aiquants/console-ui/testing`'s role scan `scanColourRoles` as exactly its own pair, its state's `success-fg` on `success-tint` or `danger-fg` on `danger-tint`, and the error text's `danger-fg` on `danger-tint` in either state (pairs `@aiquants/console-ui`'s `CONTRAST_PAIRS` lists for both schemes, so each meets the text bar).
|
|
250
300
|
No class inside the mark can change what it paints (a colour, `opacity`, a filter, a blend mode, a mask, an animation or any arbitrary property such as `[color:…]`, under any variant).
|
|
251
301
|
On the component's elements around it there is no compositing class (`opacity`, a filter, a blend mode, a mask, an animation), no arbitrary property, and no class that paints the elements inside (a colour or compositing utility under a variant that reaches them: `*:`, `**:`, `marker:`, `selection:` or an arbitrary variant with `&` such as `[&_p]:`), which would set the mark's paint directly, whatever the order of the host's layers.
|
|
252
302
|
The overall badge's icon and each badge's dot paint only `currentColor`, the mark's text colour, so they meet the 3:1 bar with the text's pair.
|
|
253
|
-
- **Qdrant storage card
|
|
254
|
-
- **Ingest console:** the `記録なし` last-run note (`ingest-last-run-none-${id}`) and the blocked-action reason (`ingest-action-blocked-${id}`) on the row at rest and hovered (`hover:bg-slate-50/80`); the `旧版` badge (`ingest-item-superseded-${id}`, `slate-600` on its own `slate-100`, 6.90:1); the required-field marks of the URI, confidentiality, and domain fields (`ingest-required-mark-${field}`, `red-700` on the form's white card, 6.42:1).
|
|
303
|
+
- **Qdrant storage card's selected look:** the four ratios of the focus ring, the tint and the start-edge bar (see `QdrantStorageCard`), recomputed in both schemes.
|
|
255
304
|
|
|
256
|
-
|
|
305
|
+
The components not yet migrated to the console foundation still style themselves with Tailwind classes or inline styles: `DocumentChangeView`, `DocumentHistoryInspector`, `DocumentIngestConsole`, `EnrichmentStrategyBoard`, `InfrastructureHealthMonitor`, `MeasuredText`, `OntologyStatusCard`, `PipelineFlowViewer`, `PipelineRunSteps`, `QdrantStorageCard`, `SyncStatusMonitor` and `TextDiffView`.
|
|
306
|
+
Each marks its root element `data-cui-migration="pending"`, `tests/migratedSources.spec.ts` keeps it on its `NOT_YET_MIGRATED` list (which may only shrink, and which no other source may join), and the console's browser checks (axe, the visual contract) leave its subtree out. The colours they write as classes or inline values are not measured: they do not reach AAA, and each component moves onto the colour roles, where the role scan covers it, in the stage that redraws its tab.
|
|
307
|
+
Colour pairs outside this section are not measured.
|
|
257
308
|
|
|
258
309
|
## Shared Presentation (`pipelinePresentation.ts`, `RecordedInstant.tsx`)
|
|
259
310
|
|
|
260
311
|
Exported for hosts that draw their own run tables, so a run reads the same everywhere.
|
|
261
312
|
|
|
262
|
-
- `ledgerStepLabel(key)` — the name of any step a run records: a flow step's `PIPELINE_STEPS` label, the terminal step `record_document_state` as `RUN_TERMINAL_STEP_LABEL` (`文書の状態の記録`)
|
|
313
|
+
- `ledgerStepLabel(key)` — the name of any step a run records: a flow step's `PIPELINE_STEPS` label, the terminal step `record_document_state` as `RUN_TERMINAL_STEP_LABEL` (`文書の状態の記録`), and a key the current vocabulary does not know (an `UnknownTerm`) as `知らない段 (<key>)`.
|
|
314
|
+
`documentKindLabel(kind)` names a run's document kind the same way (`DOCUMENT_KIND_LABELS`, or `知らない種類 (<kind>)`). `PipelineRunSteps` draws a step with an unknown key as one card, never inside a fan-out group. The flow diagram and the step metrics never show the terminal step (`PIPELINE_FLOWS` does not name it); only `PipelineRunSteps` does. `RUN_TERMINAL_STEP_OUTCOME_LABELS` words a succeeded terminal step's outcome.
|
|
263
315
|
- `documentHistoryPresentation` — the history's words for hosts: `FACET_LABELS`, `DOCUMENT_STATE_CAUSE_LABELS`, `facetCountsText(counts)`, `revisionLabel`, and the restricted marks' fixed texts `RESTRICTED_DOCUMENT_TEXT` (`隔離 (RESTRICTED) の文書`), `RESTRICTED_STATE_TEXT` and `RESTRICTED_RUN_TEXT` (`隔離されていた期間の実行`), so a host table that draws a mark row writes the same text.
|
|
264
316
|
|
|
265
317
|
- `PIPELINE_RUN_STATUS_PRESENTATION` — `{ label, className }` for `RUNNING` / `SUCCEEDED` / `FAILED` and `FAILED_RETRY_SCHEDULED` (`失敗 (再試行予定)`); pick the key with `pipelineRunPresentationKey({ status, retryScheduled })` (a retry marker on a non-failed run is ignored).
|
|
266
318
|
- `PIPELINE_STEP_STATUS_PRESENTATION` — `{ label, className, bar }` per step status.
|
|
267
319
|
- `PIPELINE_TRIGGER_LABELS` — operator-facing names of the triggers `PipelineTriggerSchema` allows: `ingest` (取り込み), `retry` (再試行), `resync` (再同期).
|
|
268
|
-
- `skipReasonLabel(text)` — reads the text through `parseSkipReason`: `gate_closed:<flag>` → `<flag> が無効`, `no_children` → `子アセットなし`, `source_skipped:<step>` → `<step label> が実行されなかった`, `nothing_to_read:<token>` → `NOTHING_TO_READ_LABELS[token]` (`no_pages` → `読む対象なし (ページなし)`, `no_pages_rendered` → `読む対象なし (描画できたページなし)`), and the terminal step's `superseded` → `実行の版が現在の版でなくなった`, `recording_disabled` → `記録の切替が無効`, `recording_misconfigured` →
|
|
320
|
+
- `skipReasonLabel(text)` — reads the text through `parseSkipReason`: `gate_closed:<flag>` → `<flag> が無効`, `no_children` → `子アセットなし`, `source_skipped:<step>` → `<step label> が実行されなかった`, `nothing_to_read:<token>` → `NOTHING_TO_READ_LABELS[token]` (`no_pages` → `読む対象なし (ページなし)`, `no_pages_rendered` → `読む対象なし (描画できたページなし)`), and the terminal step's `superseded` → `実行の版が現在の版でなくなった`, `recording_disabled` → `記録の切替が無効`, `recording_misconfigured` → `記録の切替の設定の誤り`, and the unlock step's `not_protected` → `パスワードが付いていない`; text outside the grammar is returned verbatim.
|
|
321
|
+
A fan-out step's per-child records — including a child skipped as `gate_closed:ENABLE_OCR` or `nothing_to_read:no_pages` — are grouped under the step in the step timeline, in the tree order of their targets' tree paths, each with its own reason.
|
|
269
322
|
- `RecordedInstant({ iso, testId })` — a recorded instant in the root's zone, to the second (`2026/09/24 01:02:03`), in a measured figure slot under `testId` (`data-measured="true"`, tabular figures, never broken between the date and the time); `iso: null` is the root's unmeasured label (`data-measured="false"`). A refused instant throws `ZonedTimeError` (see **Time zone is the root's**). Render it inside the Japanese console root, like every component that reads the root.
|
|
270
323
|
- `FinishedInstant({ iso, testId })` — a finish time that may still be open: `RecordedInstant` for a finished run or step, or the word `未完了` (`UNFINISHED_LABEL`) outside any slot while `iso` is `null`. It checks the root in both cases.
|
|
271
324
|
- The package publishes no function that turns an instant into text: a host table draws its instants with these two, so they share the root's clock, the slot and its hook with every panel of the package.
|