@aiquants/enrichment-react 0.7.0 → 0.9.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @aiquants/enrichment-react
2
2
 
3
- 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.
3
+ 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, the embedding variant board, and the status, health, storage, and ontology cards.
4
4
 
5
5
  ## Prerequisites
6
6
 
@@ -19,7 +19,8 @@ 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`, `EnrichmentStrategyBoard`, `PipelineFlowViewer`, `DocumentHistoryInspector`, `DocumentChangeFeed`, `ProcessingObjectBrowser`, `QdrantStorageCard`, `SyncStatusMonitor`, `OntologyStatusCard`, `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.
22
+ - **Rendered inside the console root.** `InfrastructureHealthMonitor`, `EnrichmentStrategyBoard`, `PipelineFlowViewer`, `DocumentHistoryInspector`, `DocumentChangeFeed`, `ProcessingObjectBrowser`, `QdrantStorageCard`, `SyncStatusMonitor`, `OntologyStatusCard`, `DocumentIngestConsole`, `EmbeddingVariantBoard`, 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.
23
+ The other components do not read the root and render anywhere.
23
24
  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
25
  This package ships no stylesheet and paints nothing itself (no class and no inline style; `tests/migratedSources.spec.ts` checks every source), so 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
26
  Without it the root's `--cui-*` token variables are unset, every `Measured` value (a count, a byte size, an instant) loses its tabular figures and a text that moves whole wraps like plain text,
@@ -102,7 +103,8 @@ the input (`ingest-file-uri-input`, a console `TextInput` labelled `ファイル
102
103
  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.
103
104
  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. A refusal is the input's field error (`ingest-file-uri-input-error`, with `aria-invalid`). An empty catalog shows `取り込める源が配備にありません。` (`ingest-no-sources`, a warning `Notice`) with no scheme choice, no input and no inspection, and the submit button says so (`SUBMIT_BLOCKED_REASONS.noSource`).
104
105
  `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 (a `recent-item-row` with `data-restricted-mark="ingest"` and `data-position`, its position in the response), with no history entry and no row action;
105
- 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 `失敗 (再試行予定)`, a `RUNNING` run with `retryScheduled: true` (waiting for its next attempt) reads `再試行待ち`, and a last run that is a mark reads `隔離されていた期間の実行` with its status (`ingest-last-run-restricted-${id}`).
106
+ 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`;
107
+ a `FAILED` run with `retryScheduled: true` reads `失敗 (再試行予定)`, a `RUNNING` run with `retryScheduled: true` (waiting for its next attempt) reads `再試行待ち`, and a last run that is a mark reads `隔離されていた期間の実行` with its status (`ingest-last-run-restricted-${id}`).
106
108
  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
107
109
  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
108
110
  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.
@@ -152,11 +154,13 @@ Each row lays out as every `DescriptionList` row does (the term a third of the w
152
154
 
153
155
  ### 5. `MetadataSearchWorkbench`
154
156
 
155
- 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.
157
+ The document search of the search tab (the public API's `GET /ai/search/documents`). Props: `tenantId`, `variantSets: readonly VariantSetView[]` (the tenant's variant sets, `VariantSetList.variant_sets`), `onSearch(params, signal)` and the optional `initialParams: Partial<DocumentSearchParams>`, which fills the query, the strategy, its controls, the variant set and the filters.
156
158
  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.
157
159
 
158
160
  - **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`).
159
161
  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`.
162
+ For a strategy with a vector tier (`SPARSE`, `DENSE`, `HYBRID`; `MODE_TIERS`), the embedding variant set the vector tiers read (`search-variant`): `本番 (default)` first, then the active sets in key order, each named with its projected share (`variantSetOptionLabel`: `eg2-v1 (投影済み 70.0%)`, or `(対象の文書なし)` for a set whose rows count no document). Production is the API's default and is never sent; any other set travels as `variant_key`. A chosen set that leaves the active list reads as production again.
163
+ A set that holds less than every document says so in a note (`search-variant-partial`: `組 <key> はテナントの文書の 70.0% (7 / 10 件) を投影しています。…`), since a document it lacks cannot be found through its vector tiers. The run trace names the set (`変種の組: …`), and so does the results card's description (`· 組: …`).
160
164
  - **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.
161
165
  - **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.
162
166
  - **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.
@@ -177,22 +181,21 @@ A fused value outside [0, 1] (the meter's range) or a search time that is not a
177
181
 
178
182
  ### 6. `ModelComparisonWorkbench`
179
183
 
180
- 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.
181
- 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>`.
184
+ Runs one query against 2 to 4 embedding variant sets (the public API's `POST /ai/search/compare-variants`) and lays their ranked chunks side by side in a comparison grid (`compare-grid`: a column per set in the request's order, the rows `密の層` and `疎の層` saying which tiers ran (`走った` / `走らなかった` / `この戦略では走らせない`), then a row per rank, each cell `compare-${position}-${rank}-${layout}`).
185
+ Props: `variantSets: readonly VariantSetView[]` (the tenant's variant sets) and `onCompare(request, signal)`, which answers the core's `VariantComparisonAnswer` or rejects with an `Error` whose message the workbench shows, and should stop when `signal` aborts.
182
186
 
183
- - **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.
184
- - **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.
185
- - **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`).
186
- - **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.
187
- - **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.
187
+ - **Form** (`compare-card`, with `data-running`; `compare-form`): the sets to compare (`compare-sets`, a `CheckboxGroup` of `本番 (default)` and the active sets with their projected shares, the first two chosen by default; a set that leaves the list leaves the choice), the strategy (`compare-mode`, `DENSE` or `HYBRID` by `COMPARISON_MODE_LABELS`; `HYBRID`, the API's default (`API_DEFAULT_COMPARISON_MODE`), is pressed first and never sent, so only a `DENSE` comparison names its mode), the domain (`compare-domain`, `全てのドメイン` or a text domain every chosen set other than production has a row of;
188
+ sent only when chosen), the query (`compare-query`, 1–500 characters after trimming) and the page size (`compare-limit`).
189
+ The request is built with the core's `VariantComparisonRequestSchema`. Fewer than `SEARCH_PARAM_LIMITS.comparisonVariants.min` (2) or more than its `max` (4) sets, or an empty query, say why at their fields once a send has been attempted and send nothing; fewer than two offered sets draw an info note (`compare-variants-insufficient`) instead of the form.
190
+ - **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 set's list it says `この順位の結果無し`. A column heading reads `<set> · <execution ms> ms` (production as `本番 (default)`). Scores are never normalized: two sets may embed with different models, so a cosine from one is not comparable to a cosine from another.
191
+ - **Agreement** with the first set (`compare-agreement`, a row per other set, `compare-agreement-row` with `data-variant-key`) is measured on ranks by `rankAgreement(a, b)`: the shared chunks with both counts (`compare-shared-count-${position}`), the Jaccard overlap (`compare-overlap-ratio-${position}`) and the mean absolute rank difference of the shared chunks (`compare-rank-difference-${position}`); `compare-summary` names the strategy that ran, the baseline and whether the bodies are shown (`compare-text-hidden`).
192
+ - **What did not run or is hidden:** a set whose strategy's tier did not run (`dense_ran` / `sparse_ran` false; a `DENSE` comparison runs no sparse tier and says nothing of it) says which and why in a warning of its own (`compare-degraded-${position}`, its reasons by `degradationReasonText`); a body the caller's scope hides (`text_hidden`, or a `null` text) reads `非表示 (列のスコープ)` in every cell; when no set has a chunk, `compare-empty` says so.
193
+ - **Failures:** an answer without one of the requested sets (`答えに組 <set> の結果がありません。`) 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.
188
194
 
189
- An execution time that is not a non-negative safe integer throws `RangeError` naming the strategy instead of being drawn.
195
+ An execution time that is not a non-negative safe integer throws `RangeError` naming the set instead of being drawn.
190
196
 
191
197
  ```tsx
192
- <ModelComparisonWorkbench
193
- variants={[{ strategyId: 11, label: "GENERAL_DOCUMENT / default", embeddingModel: "text-embedding-3-large" }, { strategyId: 12, label: "GENERAL_DOCUMENT / ruri", embeddingModel: "cl-nagoya/ruri-v3-310m" }]}
194
- onCompare={(input, signal) => api.compareVariants(input, signal)}
195
- />
198
+ <ModelComparisonWorkbench variantSets={variantSetList.variant_sets} onCompare={(request, signal) => api.compareVariants(request, signal)} />
196
199
  ```
197
200
 
198
201
  ### 7. `OntologyStatusCard`
@@ -253,12 +256,18 @@ The viewer draws with `catalogFigureGeometry(catalog)` (`PipelineFlowViewer.tsx`
253
256
  A non-modal panel showing one document's change history across its revisions (S1 §3.3.5-13, §8.19): a console `Card` (`history-inspector`, `data-metadata-id`), the region named by its `h2`, with no `role="dialog"`. It reads through the host and draws only what the host returns.
254
257
 
255
258
  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;
256
- 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 }>`).
259
+ 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;
260
+ 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`, the optional `captureState(metadataId)` (the host's manual capture, a `ResourceOutcome<{ outcome }>`), and the variant section's `variantSets: readonly VariantSetView[]`, `variantHost: DocumentVariantHost` and `canSendVariantCommands`.
257
261
 
258
262
  - **Header:** the card's `h2` `文書の履歴 — <登録名>` (the level-2 heading of `history-inspector`; the foundation's card title carries no test id), the close button in the card's actions (`history-close`), and a `DescriptionList` (`history-document`, one row `history-document-<key>` each) with the URI, the current revision (`r3`), the confidentiality level, the number of revisions and the last change's time.
259
263
  - **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 (each named `実行 #<id> · <きっかけ> · <流れ>`, the flow by `pipelineFlowLabel` (`history-row-<key>-flow`), 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>"`).
260
264
  Marks are written as sentences: `失敗した実行の後の状態`, `記録されていません` (a finished run with no terminal step or a failed one), `この変化には記録されなかった実行 #n の変化も含まれます`, and `ソースは同じで、モデルの出力だけが変わりました`.
261
265
  Restricted marks (V11) sit in their own group ahead of the revisions, `隔離されていた期間の記録 (n 件)`, closed at first, one row each in the foundation's bulleted `List` (`history-restricted-list`, the toggle's `aria-controls` target; items `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.
266
+ - **Variant sets:** below the header, a `Disclosure` `変種の組での状態 (n 組)` (`history-variants`, closed at first, closed again for another document) holds the document's state in each active set; the section reads `variantHost.load(metadataId)` (`DocumentVariantReading`: the document's text domain and core's `DocumentVariantState`s) only while it is open, and again on each `openRequest`.
267
+ A row per active set (`history-variant-row`, `data-variant-key`, and `data-state`, which names what its state cell reads: a state, `unread`, `unclassified` or `missing`) first decides whether the set covers the document by the reading's domain (`variantSetCoverage`, the rule the processing-objects list reads too).
268
+ A set that does not cover it reads `未分類` (the document's domain is not known yet) or `対象外` (the set has no row of the domain) in the state column (`VARIANT_UNCOVERED_STATE_TEXTS`, never a badge) and says why there is no projection (`文書のドメインが決まっていません`, `組はこの文書のドメインの行を持ちません`).
269
+ A covered set shows the state as a badge (`PROJECTION_VARIANT_STATE_LABELS`: `投影済み`, `古い版を投影済み`, `投影に失敗`, `未投影`), or `状態を読んでいません` when the reading lacks the set, and `今すぐ投影` projects the document with the set's row of the document's domain (`variantHost.project(metadataId, strategyId)`, the `strategy_id` of `VariantSetView.domains`); `点を消す` (only for a set that may hold points) calls `variantHost.removePoints(variantKey, metadataId)`. The state column is as wide as the widest of these words (`VARIANT_STATE_CELL_CONTENTS`).
270
+ Only the pressed command is busy; a settled one is announced and reads the states again; a refused one says why under the table (`history-variant-failure-${key}`), the 423 refusal of a set with a waiting or running projection job in a sentence of its own that carries the API's reason naming the job (`variantProjectionActiveSentence`). With `canSendVariantCommands` false, both commands are unavailable from the first render, described by `history-variants-forbidden-note`.
262
271
  - **Detail (`history-detail`, `data-selected-key`):** a console `CardSection` (not a landmark) with one `h3` (its level-3 heading, the same element whatever it shows), the selected item's summary as a `DescriptionList` (`history-state-summary` or `history-run-summary`, one row `<list>-<key>` each, the run's status `Pill` `history-run-status` with `data-status` and `data-presentation`) and the selected state's change from its predecessor (`DocumentChangeView`: a `nav` of changed facets, one `CardSection` at `level={4}` per changed facet (an `h4`, not a landmark, `data-facet`),
263
272
  `変化なし` for the rest; the structured data's JSON values in the mono face with their line breaks and indentation kept; 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>`).
264
273
  Every control sits on a line of its own (a `Cluster`, or the card's actions), so no 44 px target stretches a line of text; a timeline row's lines (the run or cause, the time, each facet badge, the marks, the pin) are the row's lines, and a line mixing a `Pill` and text is one `Text` span.
@@ -293,6 +302,7 @@ each change reads the history again, keeping the shown one until the answer sett
293
302
  openRequest={entry.openRequest} onClose={closeHistory}
294
303
  returnFocusFallback={() => tabRefs.current[activeTab]} captureState={captureDocumentState}
295
304
  canSendCommands={operatorMaySendCommands}
305
+ variantSets={variantSetList.variant_sets} variantHost={documentVariantHost} canSendVariantCommands={operatorMayManageVariants}
296
306
  />
297
307
  ```
298
308
 
@@ -346,12 +356,15 @@ The recent document changes (S1 §3.3.5-4), a full-width `Card` `直近の文書
346
356
  ### 13. `ProcessingObjectBrowser`
347
357
 
348
358
  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.
349
- Props: `host: ProcessingObjectHost` (`{ loadPage(filter, cursor, signal) }`, `processingObjectHost.ts`), `mimeKindRules` (the flow catalogue's `mime_kind_rules` in rule order, which name a document row's kind; required, the list has no rules of its own), the controlled `filter: ProcessingObjectFilter` with `onFilterChange(filter)`, and the optional `onOpenHistory(metadataId)` (without it there is no `履歴` column).
359
+ Props: `host: ProcessingObjectHost` (`{ loadPage(filter, cursor, signal) }`, `processingObjectHost.ts`), `mimeKindRules` (the flow catalogue's `mime_kind_rules` in rule order, which name a document row's kind; required, the list has no rules of its own), the controlled `filter: ProcessingObjectFilter` with `onFilterChange(filter)`, `variantSets: readonly VariantSetView[]` (the tenant's variant sets), and the optional `onOpenHistory(metadataId)` (without it there is no `履歴` column).
350
360
 
351
361
  - The filters: three `SegmentedControl`s (`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`, `絞り込む`).
352
362
  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`.
353
363
  - 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.
354
364
  - The count `全 N 件のうち M 件を表示しています。` (`processing-objects-count`) is written above the table and announced (`useAnnounce`) whenever it changes.
365
+ - Each active variant set adds, above the table, a meter of its projected share (`processing-objects-variant-coverage`, `processing-objects-coverage-${key}`: Σprojected / Σdocuments over the set's rows, `87.5% (7 / 8 件)`, or `対象の文書がありません`), and a column `組 <key>` with a document's state in the set (`processing-object-variant-${id}-${key}`).
366
+ Whether the set covers the document is decided first, by the document's text domain (`ProcessingObjectItem.domainKey`, `variantSetCoverage`): a document it does not cover reads `未分類` (no domain yet) or `対象外` (the set has no row of the domain), never a badge, even when the page carries a state for the set; a covered one reads its state's badge with `data-state`, or `状態を読んでいません` when the page lacks the set.
367
+ The column is as wide as the widest of these words (`VARIANT_STATE_CELL_CONTENTS`); a node and a restricted mark leave it empty.
355
368
  - A row (`processing-object-row`, `data-row-kind` `document` / `node` / `restricted`) shows the id, the kind (`文書 (<document kind>)`, the kind `classifyDocumentKind(mimeKindRules, mimeType)` gives the recorded MIME type, `その他` for a type no rule names; or the asset type's name), a node's place (`treePathLabel` and `根 <id>`), the name (a document's name and URI, a node's `sourceName` or `名前なし`), the classification and revision (`CONFIDENTIALITY_LEVEL_LABELS`, `r<n>`, `旧版`, `削除済み`), the status and the times (`登録` / `更新`, `RecordedInstant`).
356
369
  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).
357
370
  `履歴` opens the document's history (`文書の履歴`), and a node's opens its root's (`根の履歴`); the button names its object to assistive technology.
@@ -383,19 +396,39 @@ The ingest tab's board of the deployment's moving ingestion jobs (S1 §3.3.5-6,
383
396
  Props: `read: IngestionJobPageReader` (`(state, after, signal) => Promise<ResourceOutcome<IngestionJobPage>>`; `state` `RUNNING` or `QUEUED`, the first page with `after` `null`), `revision` (advanced by the host on the jobs' signals).
384
397
  Also `canSendCommands` (whether the operator may send a cancel at all; when false each row's 取り消す is unavailable from the first render, described by one note before the sections, `ingestion-job-board-forbidden-note`, `INGESTION_JOB_CANCEL_FORBIDDEN_SENTENCE`, drawn while some section draws a 取り消す; no confirmation opens), `isBusy` (a submission of the host is in flight), `cancellingJobId` (the job whose cancel is in flight, or `null`), `onCancel(job)` and `cancelAnswer` (the last cancel's answer, `IngestionJobCancelAnswerSchema`'s job, or `null`).
385
398
 
386
- - A row (`<section>-row`, `data-job-id` and `data-state`, or `data-restricted-mark="job"` for a mark) shows what the job acts on (`ingestionJobTargetLabel`: `「<name>」(r<revision>・#<row>)` with `旧版` for an older revision, or the kind for a job on no single document), its kind (`ingestionJobKindLabel`: `取り込み` / `取り下げ` / `日報の照合`, `知らない種類 (<kind>)` otherwise), its state (`ingestionJobStatus`: `待ち`, `再試行の待ち`, `実行中`, `取り消しを求めた実行中`), its attempts (`<started> / <most>`), when it was enqueued, when a worker may take it next (waiting jobs only, `—` while running),
399
+ - A row (`<section>-row`, `data-job-id` and `data-state`, or `data-restricted-mark="job"` for a mark) shows what the job acts on (`ingestionJobTargetLabel`: `「<name>」(r<revision>・#<row>)` with `旧版` for an older revision, or the kind for a job on no single document), its kind (`ingestionJobKindLabel`: `取り込み` / `取り下げ` / `日報の照合` / `変種の組の投影`, `知らない種類 (<kind>)` otherwise), its state (`ingestionJobStatus`: `待ち`, `再試行の待ち`, `実行中`, `取り消しを求めた実行中`), its attempts (`<started> / <most>`), when it was enqueued, when a worker may take it next (waiting jobs only, `—` while running),
387
400
  its last failure (`—` without one) and 取り消す while it waits or runs and no cancel was asked.
388
401
  - A restricted mark shows `RESTRICTED_DOCUMENT_TEXT`, its state and its enqueue time only, and offers no cancel; a job whose document the row scope hides is not on the board. An empty section reads `読める<section>の仕事はありません。`, and an empty page that is still followed `ここまでに読める<section>の仕事はありません。続きを読み込めます。`.
389
402
  - 取り消す (`aria-expanded`, `aria-controls`) opens the section's confirmation above its table (`<section>-confirm`, a named group) with `ingestionJobCancelSentence` and focus on 取り消しを確かめる (`danger`), which calls `onCancel`; やめる closes it and returns focus to 取り消す. No table cell holds an unavailable control: the confirmation's button tells why it cannot send (`他の送信の答えを待っています`, `別の仕事の取り消しの答えを待っています`) and is busy (`取り消しを送っています…`) while its own cancel is in flight.
390
403
  - Each new `cancelAnswer` is applied once in place (a job that ended leaves its section; a running job whose stop was asked shows `取り消しを求めた実行中` and keeps its document) and announced (`ingestionJobCancelAnnouncement`); an answer equal to the last one is neither applied nor announced again. Focus on a control that disappears returns to the confirmation's opener when it remains, else to the section's heading.
391
404
  - Each `revision` re-reads a section's first page, a refused one too (a 403 offers no reload, so the next revision brings it back once the refusal is lifted), except a section whose later pages the operator read: it keeps them and offers `最新の一覧に読み直す` (`<section>-stale`). A failed re-read or later page keeps the rows shown and says why (`<section>-refresh-failed`); a later page's 403 re-reads the first page, and a 403 there withdraws the section (`動いている仕事を閲覧する権限がありません。<原因>。`, `INGESTION_JOBS_REFUSAL`).
392
405
 
406
+ ### 17. `EmbeddingVariantBoard`
407
+
408
+ The board of the tenant's embedding variant sets (the API's `GET /ai/enrichment/admin/variants`, `VariantSetList`): one card `埋め込みの変種の組` (`embedding-variant-board`) with a `CardSection` per set (`variant-set-${key}`, `data-active`, `data-auto-project`; its `h3` `組 <key>` and its identity label) and the create form (`variant-create`). Props: `variantSets: readonly VariantSetView[]`, `commands: VariantSetCommands` (`variantHost.ts`) and `canSendCommands`. The host re-reads the list after a command; the board holds no copy of a set.
409
+
410
+ - **A set:** its embedding (`variant-set-embedding-${key}`: the provider by `EMBEDDING_PROVIDER_LABELS`, the model, the dimension, the query prefix quoted so its trailing space shows, the passage template, the sparse modifier by `SPARSE_MODIFIER_LABELS`, the endpoint and its key's variable (`無し (宛先の決まった種類)` without one), the identity) and its projected share (`variant-set-coverage-${key}`, a meter of Σprojected / Σdocuments, or `対象の文書がありません`);
411
+ the switches `自動の投影` and `有効` (`variant-set-auto-project-${key}`, `variant-set-active-${key}`), each sending only the switch it changes (`commands.patch(key, { auto_project })` / `{ is_active }`); the rows per text domain (`variant-rows-${key}`, `variant-row-${key}` with `data-domain-key` and `data-strategy-id`: the domain with its collection and indexing logic, the strategy, `文書 n 件 · 投影済み n · 古い版 n · 失敗 n` and the row's share).
412
+ - **Projection:** the scope (`variant-set-scope-${key}`, `未投影の文書だけ` first, or `全ての文書をもう一度`) and `投影を始める` (`variant-set-project-${key}`, `commands.startProjection(key, { scope })`; unavailable for an inactive set). The 202 answer is noted under the set (`variant-set-noted-${key}`) with its job, or that it merged into a job already waiting or running.
413
+ - **Removing every point:** `全ての点を消す` (`variant-set-remove-${key}`, `aria-expanded`) opens a confirmation group (`variant-set-confirm-${key}`) with focus on `全ての点を消すことを確かめる` (`danger`), which calls `commands.removePoints(key)`; `やめる` closes it. Focus on a confirmation that disappears returns to its opener, or to the card's heading when the set left the list. A 423 refusal (a projection job of the set is waiting or running) is the sentence `variantProjectionActiveSentence` with the API's reason naming the job.
414
+ - **Create:** every field of `VariantSetCreateRequest` (`variant-create-key`, `-provider`, `-model`, `-dimension`, `-query-prefix`, `-passage-template`, `-sparse-modifier`, `-base-url`, `-api-key-env`, `-auto-project`); the texts are sent as typed, an empty optional text as `null` and the dimension as a number. The form is checked with the core's `VariantSetCreateRequestSchema` before it is sent, and each refused field says why (`VARIANT_SET_FIELD_MESSAGES`) once a send has been attempted.
415
+ A refusal the host reads (`VariantSetCreateRejection`) puts the API's 422 messages at the fields they name and the rest in `variant-create-rejected`, and an endpoint outside the deployment (403 / 4033) at the URL field (`ENDPOINT_OUTSIDE_DEPLOYMENT_TEXT`); any other failure is `variant-create-failure`. A created set is announced and the form empties.
416
+ - Only the pressed control is pending while its request is in flight (a switch draws `切り替えを送っています…` beside it, a button is busy); each set's last outcome is noted or alerted under it (`variant-set-failure-${key}`). With `canSendCommands` false, every control is unavailable from the first render, described by `variant-board-forbidden-note` (`VARIANT_COMMAND_FORBIDDEN_SENTENCE`), and nothing is sent.
417
+
418
+ ## Variant Sets (`variantPresentation.ts`, `variantHost.ts`)
419
+
420
+ The words every variant component draws, each a total map over `@aiquants/enrichment-core`'s vocabulary: `PROJECTION_VARIANT_STATE_LABELS` and `PROJECTION_VARIANT_STATE_TONES`, `EMBEDDING_PROVIDER_LABELS`, `SPARSE_MODIFIER_LABELS`, `COMPARISON_MODE_LABELS`, `VARIANT_PROJECTION_SCOPE_LABELS`, with `PRODUCTION_VARIANT_LABEL` / `variantKeyLabel`, `variantSetOptionLabel`, `VARIANT_STATE_UNREAD_TEXT`, `VARIANT_UNCOVERED_STATE_TEXTS`, `VARIANT_SET_NO_DOCUMENT_TEXT`, `VARIANT_COMMAND_FORBIDDEN_SENTENCE` and `variantProjectionActiveSentence`.
421
+ Whether a set covers a document is one rule, `variantSetCoverage(set, domainKey)` (`VariantSetCoverage`): `unclassified` when the document's domain is `null`, `missing` when no row of the set (`domains[].domain_key`, matched exactly) is the domain, else `covered` with that row (the strategy row a projection names).
422
+ `ProcessingObjectBrowser` and the history panel's variant section both read it: an uncovered set's state cell is the word of its kind (`VARIANT_UNCOVERED_STATE_TEXTS`: `未分類`, `対象外`), and both state columns are measured from `VARIANT_STATE_CELL_CONTENTS` (every state's badge, `状態を読んでいません`, `未分類` and `対象外`).
423
+ `activeVariantSets(sets)` picks the sets a search, a comparison, a document's states and the processing-objects list name. A set's share is one formula, `variantCoverage(rows)` (Σprojected / Σdocuments over its rows, `null` when they count no document), written by `variantSharePercentText` rounded down to `VARIANT_SHARE_DECIMALS` (1) decimals on the integer counts, so `100.0%` means every document is projected, and by `variantCoverageText` with the counts in the root's format.
424
+ The host contracts are `VariantSetCommands` (the board's four commands: `create` → `CommandOutcome<VariantSetView, VariantSetCreateRejection>`, `patch` → `ResourceOutcome<VariantSetView>`, `startProjection` → `ResourceOutcome<VariantProjectionAccepted>`, `removePoints` → `CommandOutcome<null, VariantProjectionActiveRejection>`) and `DocumentVariantHost` (`load` → `ResourceOutcome<DocumentVariantReading>`, `project(metadataId, strategyId)` → `ResourceOutcome<DocumentVariantProjection>`, `removePoints(variantKey, metadataId)` → `CommandOutcome<null, VariantProjectionActiveRejection>`).
425
+
393
426
  ## Search Exchange Shapes (`documentSearch.ts`)
394
427
 
395
- The shapes the two workbenches exchange with their host, field for field the public API's answers, and the words they are drawn with:
428
+ The shapes the document search exchanges with its host, field for field the public API's answers, and the words both search workbenches are drawn with:
396
429
  `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`),
397
- `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`).
398
- `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.
430
+ and `DocumentSearchParams.variant_key` (sent only for a set other than production). The comparison exchanges the core's `VariantComparisonRequest` and `VariantComparisonAnswer` as they are.
431
+ The document search's `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; `DEGRADATION_REASON_LABELS` words every code of the core's vocabulary, `dense_embedding_unavailable` (the query's embedding endpoint was not reached, so the dense tier was skipped) included.
399
432
  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`.
400
433
 
401
434
  ## Colour Contrast