npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

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

About

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

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

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

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

Open Software & Tools

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

© 2026 – Pkg Stats / Ryan Hefner

@aiquants/enrichment-governance-react

v0.8.0

Published

React UI components for enterprise knowledge governance: CitationProofBadge, DryRunDiffViewer, KnowledgeGraphExplorer, SystemParameterTable, FeatureFlagBoard, PromptTemplateConsole, ClassificationRuleEditor, TriStoreIntegrityAudit, PolicyConflictSummaryCa

Readme

@aiquants/enrichment-governance-react

React UI components for enterprise knowledge governance: CitationProofBadge, GovernanceDryRunConsole, KnowledgeGraphExplorer, SystemParameterTable, FeatureFlagBoard, ConfidentialityPolicyBoard, PromptTemplateConsole, ClassificationRuleEditor, TranscriptionAdaptationBoard, TriStoreIntegrityAudit, PolicyConflictSummaryCard, PolicyConflictQueue, DocumentAuthorityRegistry and FilePasswordBoard — plus the interactive graph figures they draw on (InteractiveGraphCanvas, GraphNeighborhoodMap).

TriStoreIntegrityAudit, PolicyConflictSummaryCard, PolicyConflictQueue and DocumentAuthorityRegistry are built on @aiquants/console-ui: render them inside the console's one ConsoleUiRoot (they throw outside it) and load its stylesheet (see the console-ui README). They paint only through the foundation's colour roles, write every instant with the root's formatLocale and timeZone (formatInstant) and size every instant column (and the minimum of every flexible column) from an instant so written (160 px for ja-JP 2026/12/31 23:59:59, 168 px for en-US 12/31/2026, 23:59:59) and every tile and count column from the longest count so written (9,999,999 in ja-JP, 9 999 999 in fr-FR), and announce through the root's live region.

Prerequisites

  • Node.js >= 18.0.0
  • React >= 19.0.0 and React DOM >= 19.0.0 (peer dependencies)
  • @aiquants/console-ui
  • @aiquants/enrichment-core
  • @aiquants/enrichment-governance-core

Installation

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

Exported Components

  1. CitationProofBadge: Visual badge for cryptographic Merkle citation proof status and groundness.
  2. GovernanceDryRunConsole: Prompt / parameter / feature-flag override editor that runs a dry-run simulation against golden benchmarks and shows the score deltas.
  3. KnowledgeGraphExplorer: Knowledge graph explorer with the interactive label topology figure, a node index, relationship traversal (results drawn as a GraphNeighborhoodMap), a bitemporal timeline, and the HITL approval queue (isResolving disables the merge / promote buttons). Traversal is enabled by the traversal prop (GraphTraversalHost: { run: GraphTraversalRunner; listRelationshipTypes: GraphRelationshipTypesReader }); without it no traversal form is drawn. The two come together: the offered relationship types are the deployment's answer (its governance allow-list ∩ the knowledge relationships measured in the graph — never the measured relationship counts of schema, which include the document structure HAS_VERSION / CONTAINS_CHUNK that a traversal never walks), read once when the form mounts and again when the host hands over a different reader (GraphRelationshipTypesOutcome: { ok: true, relationshipTypes } or { ok: false, reason }, whose reason is a phrase without a closing 。 that the component places inside its own sentence 辿れる関係種別を読めませんでした (<reason>)。, so it does not restate the failure; a rejection reads as a failure with its message).
    • While the offer is being read, kg-traversal-types-loading (role="status") is shown and the submit button is disabled.
    • Offered types render as check boxes kg-traversal-edge-<type> inside kg-traversal-edge-types; none checked means every offered type (the request omits edgeTypes). A selection that leaves the offer is dropped when the offer is re-read.
    • An empty offer means nothing can be traversed in this graph yet: kg-traversal-nothing-to-traverse (role="status") says a traversal walks only the documents describing an entity (DESCRIBES_ENTITY) and allowed entity-to-entity relationships, never the document structure, and the submit button stays disabled.
    • An offer that cannot be read shows kg-traversal-types-failure with the reason; no type can be chosen, and a traversal still runs without edgeTypes (the deployment's default is the offer). A traversal that returns no rows shows exactly one note, kg-traversal-empty (起点から辿れる関係はありませんでした。), and no map or table; its kg-traversal-empty-reason states what a traversal walks — the documents describing an entity and the allowed entity-to-entity relationships — and why an answer is empty: a traversal that follows the documents returns at least the visible documents describing the start, so an absent start and a start no visible document describes are the reasons (the two are not told apart); when types were chosen it names them and adds that a start without relationships of those types also answers nothing. The map and the raw-row table (kg-traversal-table) appear only for a non-empty answer. The map is centred on the start as an Entity node (start: { canonicalId, label: "Entity" }), and only Entity nodes can be picked as the next start: the documents (File) a traversal passes through are drawn but not offered, since a document URI as the start answers nothing. The offer is read again when the host hands over a different reader; an answer (or a rejection) of the previous reader that arrives late is ignored, so it never overwrites the newer offer or clears the selection. The start field (kg-traversal-start-field) grows to 280 px and shrinks with a narrower panel. The raw-row table's columns are the keys the rows carry, in first-seen order, computed once per render; the table is memoised on the result rows, so typing in the start field (which keeps the previous result on screen) does not re-render it. The traversal's start node is called 起点 on every surface (the start field, the node index's 探索の起点にする button, the map's notes); the direction columns of the relationship tables (topology and neighbourhood alike) are 関係元 / 関係 / 関係先, so no header reuses the start's name.
  4. SystemParameterTable: Table for safely editing system parameters. bool rows read the switch from the declared type (true / 'true' / '1' on, false / 'false' / '0' off); any other value renders an inline invalid-value note (param-invalid-<key>) and disables the switch.
  5. FeatureFlagBoard: Switchboard for system feature flags with a confirmation modal for critical flags. A flag key (flag-key-<key>) wraps anywhere inside its name column (flag-name-column-<key>, min-width: 0), the 重要 badge never breaks, and the switch (flag-switch-<key>) never shrinks, so an unbroken key cannot push the switch past the card's edge.
  6. PromptTemplateConsole: Editor for prompt templates and their structured-output schema. When the templates prop delivers a new version for the active template, the editor reloads from it, so a saved template never keeps showing its pre-save copy.
  7. ClassificationRuleEditor: Priority reordering and condition editor for classification rules. The create form's target domains come from the required domainOptions prop ({ key, label }[]); an empty list makes the form say that no domains are configured.
  8. ConfidentialityPolicyBoard: Capability × confidentiality-level policy grid with a confirmation step before enabling cloud providers.
  9. TranscriptionAdaptationBoard: Custom vocabulary and reference-material management for transcription adaptation. The add forms (vocabulary-form, reference-form) are wrapping flex rows: every field asks for a fixed basis that is also its minimum width, so at narrow widths (e.g. the 327 px content box of a 390 px viewport) a field moves to the next line instead of widening the page; long identifiers and URIs in the tables break inside their cells.
  10. TriStoreIntegrityAudit: The integrity audit card of a persisted audit run that compares the SQL Server source of truth with Qdrant and Neo4j.
    • Props: overview (IntegrityAuditOverview of @aiquants/enrichment-core), canReadFindings (the operator may read every document; it decides the findings cells, the repair, and whether an open findings inspector stays open), command (IntegrityAuditCommandState: idle, starting with its mode, cancelling), commandFailure (IntegrityAuditCommandFailure, { failure, occurrence }: the core's IntegrityCommandFailure under failure and an occurrence number that advances with every failure; after a failure for which the exported integrityFailureNeedsOverviewReread(failure) is true — a run already active or already finished, or an upstream failure whose outcome is uncertain — the host re-reads the overview, so the host holds no copy of the rule), onStartRun(mode, basisRunUid), onCancelRun(runUid), loadFindings (IntegrityFindingsLoader; its identity may change on every render, because the card calls the latest one through a stable function and the memoized findings inspector is not redrawn on each poll), refreshFailure (a later read failure; the shown overview stays), headingRef (the card heading), and three optional host hooks for the findings inspector: onShowHistory(metadataId) (a row whose document is known offers 履歴), resync (the exported IntegrityFindingResync, { onResync(metadataId), onDismiss(), feedback, sending, cellResync? }: 再同期 and 再試行 both press onResync, a pipeline retry of one document; feedback is the exported InspectorResync the host owns — pending, accepted or refused with the refusal's error — replaced on every transition, and the inspector calls onDismiss() once whenever it goes away: a cell switch, 閉じる, Escape, a newer run, or a move to another tab; sending is the host's own flag that its route for the inspector's presses is sending (while it is true every row's 再同期 / 再試行 and the bulk resync are busy, drawn without a reason so they fit the table cells, because a press would replace the one in flight); cellResync is the optional bulk resync, the exported IntegrityFindingCellResync, { onResyncCell(request), feedback }, below) and remedyDestinations (the exported RemedyDestinations: strategies, projectionPolicy and failedSync, each { label, onOpen(), closesInspector }). Every function's and object's identity may change on every render: the card calls the latest one through a stable function, so the memoized inspector is not redrawn on each poll. A hook the host leaves out removes its controls and drops the remedy clauses that name them.
    • The verdict badge (audit-verdict-badge, data-verdict) and the last measurement (audit-last-measured): time, elapsed time (counted from overview.readAt and advanced by the monotonic clock every ELAPSED_TICK_MS (exported), never the wall clock, so a server render hydrates with the same text; a new readAt restarts the count), mode, requester, executor and the first 8 digits of the store fingerprint; a repair run's badge reads 修復前: <verdict>.
    • Each store section (audit-store, data-store) holds six tiles (audit-tile, data-group) that split the core's discrepancy classes into groups (INTEGRITY_TILE_GROUPS; a tile is 未測定, never 0, when any contributing quantity was not measured; a repair run counts what the repair left) and a quantity table whose positive findings-class counts (audit-cell) open the findings inspector (integrity-finding-inspector, keyset pages of 100, data-run-mode) for an operator who may read every document. The six groups, in drawing order, share whether a repair acts on them (every class of a group has the same answer in the core's INTEGRITY_CLASS_POLICY): 孤児・削除の不整合 (ORPHAN, TOMBSTONE_EXPIRED, UNPROPAGATED_DELETE), 版・配置・印の不整合 (STALE_REVISION, MISPLACED, OUT_OF_TIER, UNMARKED_SUPERSEDED, FLAG_DRIFT; a repair purges, flags and rewrites them), 形の不整合 (DETACHED, UNKEYED; a repair removes them), 変種・共有の不整合 (VARIANT_FLAG_DRIFT, UNKEYED_ENTITY; outside the repair, the sublabel reads <unit> (修復の対象外)), 欠落・未投影・不一致 (the documents of MISSING, UNPROJECTED and COUNT_MISMATCH, re-projected) and 削除保留 (TOMBSTONE, never an alarm). Every label fits 10 em, so the tile's minimum width stays 144 px. UNPROJECTED (未投影) is a document whose store has neither a projection record nor a live item while its tier is open, and UNKEYED_ENTITY (共有の鍵なし) a shared Entity without its key, which the repair never removes (a shared node may carry another system's master key).
    • The inspector's document cell adds a second line for a known document: #<id>, 履歴, and 再同期 (no projection record, a placement row whose document has no domain, a row a repair skipped as UNRECORDED or OUTSIDE) or 再試行 (an earlier pipeline stage failed, as a class or a skip reason); their accessible names name the document (文書 #29 (r1) を再同期, the revision only when positive). While the host's resync is pending every 再同期 / 再試行 is busy, the pressed one reading 再同期を送っています / 再試行を送っています, and 履歴 stays pressable; accepted shows and announces once 文書 #<id> の取り込み直しを受け付けました。処理の後に「監査を実行」で確かめます。 (integrity-finding-resync), refused raises one alert per press (文書 #<id> を取り込み直せませんでした: <error>). Both are drawn in the pressed row's document cell, under its document line, never under the table: a page holds up to 100 rows, so a message there would be off-screen for a press near the top. An inspector shows the host's state only after a press of its own: a cell switch mounts the next inspector before the host applies the dismissal, so the next inspector receives the earlier state and neither shows nor reads it. On a cell whose recorded documents a resync settles (the classes the core's IntegrityCellResyncRequestSchema accepts: UNRECORDED), when the host passes cellResync and at least one finding is recorded, the inspector offers 記録された <k> 文書をまとめて再同期… (integrity-finding-bulk-resync-open, after the remedy list and before the table; the cell's remedy then adds 「まとめて再同期」で一度に取り込み直せます。). It opens a non-modal confirmation (integrity-finding-bulk-resync-confirm, a group named まとめて再同期の確認: 100 文書ずつ流し直します。…記録の外の文書は次の監査に現れます。, the chunk size read from the core's INTEGRITY_FINDINGS_PAGE_LIMIT_MAX) with 再同期を開始する and やめる. While it is open every row's 再同期 / 再試行 is busy, and 再同期を開始する is busy (drawn without a reason) while a row's press is pending or sending is true: either press would replace the other in flight. Starting sends the first chunk, onResyncCell({ runUid, store, scopeKey, itemClass }); the host replaces its state, the exported InspectorCellResync ({ settled, status, requeued, skipped, nextAfter, error }), with a fresh pending one in the same event and accumulates every chunk's answer into it (settled counts the settled chunks). While it is pending the inspector draws a progress bar (まとめて再同期の進み, settled documents / k) with 止める, every row's control is busy, and whenever settled grows with a nextAfter it sends the continuation { …, after: nextAfter } exactly once per settlement number (a re-render, a copy of the same state or React's StrictMode never sends it twice); after 止める, or once the list is withdrawn by a 403, it sends none. done shows and announces once まとめて再同期を終えました: 再投入 a 文書・据え置き b 文書。処理の後に「監査を実行」で確かめます。, a stop shows まとめて再同期を止めました: …残りの文書は送っていません。, a refusal raises one alert per start (まとめて再同期を続けられませんでした: <error> それまでに…), each followed by the kept-back documents (据え置いた文書, 文書 #<id>: <reason>). Focus moves from the pressed control to its replacement (the start, 止める, the open control) and, when a chunk ends the run while 止める holds it, to the open control. As with a row's resync, an inspector shows the host's bulk state only after a start of its own. A placement row's observation adds its document's domain (integrity-finding-domain: ドメインが未定, ドメイン <d> に本番の戦略なし, ドメイン <d> の戦略が段を閉じています); a cell mixes domains, so no remedy names one.
    • Above the rows, after the counts, a remedy list (integrity-finding-remedies, each 「<name>」: <remedy>): the cell's remedy (integrity-finding-remedy; a scan says what a repair would do or what the operator does, a repair's acting class says where the outcome is and how to check it), and for a repair one entry per skip reason (integrity-finding-remedy-skip-<reason>) and per error class (integrity-finding-remedy-failed-<class>) the read rows carry. A remedy that names a host place adds a link 「<label>」へ移る, followed after a space by (この所見の表示は閉じます) when the move closes the inspector. A finding that recurs is handed to a developer with the run ID and the item key; an item outside the store's expectation is never offered for deletion.
    • The start (audit-start-scan), repair (audit-start-repair, a confirmation naming the basis scan, its retention and the purge / flag / re-project counts) and cancel (audit-cancel-run; 中断として閉じる with a confirmation while the heartbeat is stale) commands are sent by the host; a failure is shown with the fixed sentence of its command's vocabulary. An upstream failure is classified as every console card classifies a failed command: a client-error status says 監査の要求に失敗しました (HTTP <status>)。, while no answer or any other status (the request may have taken effect) says that the screen was updated, as the host re-reads the overview, and what to check on it (表示を更新しました。監査が始まったかどうかは、実行の状態で確かめてください。, …中止が受け付けられたかどうかは、…), like the sentences of a run already active or finished (…表示を更新しました。). Unavailable buttons keep focus (aria-disabled) and name their reason as their description.
    • The repair confirmation states that re-projection runs on the starting operator's credentials until they expire, without naming a provider's lifetime. It names the tier leftovers it purges (audit-repair-out-of-tier: cancel, open the tier and audit again to keep them; an item whose tier opens during the repair is skipped), the documents among the re-projected ones that were never projected to a store (audit-repair-unprojected: 再投影のうち未投影の文書 (Qdrant a · Neo4j b) は、段を開いた後の初めての投影です。, counted per store), the shape discrepancies it removes (audit-repair-shape-purge: 親の辺のない旧版 c 件と鍵のない項目 d 件を除きます。除いた履歴と項目は戻せません (SoT の行は残ります)。, bounded by the basis counts), the discrepancies no repair acts on (audit-repair-unrepaired: 変種・共有の不整合 N 件は修復の対象外です。) and the drifted items (audit-repair-reproject-items): rewritten through their documents' re-projection, or, when the basis re-projects no document, all skipped with a reason — a drift-only basis still offers the repair. The quantities a run could not measure are listed under 測定できなかった項目 as a list (role="list"; each audit-unmeasured-reason a role="listitem" with data-store and data-scope-key), spaced by the foundation's tokens.
    • Only phase changes and endings are announced; focus moves only when the element holding it disappears (a cancelled run's button, an ended run's section, a recovered heartbeat's confirmation, an inspector closed by a new completion or by canReadFindings turning false, which is announced as 所見を開く権限がなくなったため所見の表示を閉じました。).
    • While a run is live, 監査を実行 says 監査が実行中です。 and 修復を実行… says what it waits for (実行中の監査が終わると修復できます。), so the two buttons never draw the same reason side by side.
  11. InteractiveGraphCanvas: Generic pannable / zoomable SVG canvas for node-and-edge figures (see below).
  12. GraphNeighborhoodMap: Radial figure of one traversal's results around its start node, with the same content as an accessible table (see below).
  13. PolicyConflictSummaryCard: The policy-conflict counts of another tab (the data-retention tab, for example: four state tiles, why a count is not measured, partial counts with failed tie groups, the row-scope note, staleness) and a link (onOpenKnowledgeTab) to the host's tab where conflicts are decided. The package does not know the host's tabs: the link and the stale note name the tab by knowledgeTabLabel, and the stale note sends only an operator who may detect (viewer.canDecide) to that tab, telling any other operator that the counts may differ from the ledger. Without a detection record the card says only that (規程衝突の検知の記録がありません。, true before the first detection and after the record was lost) and sends an operator who may detect to the tab, telling any other operator that the counts come after the next detection. The link appears only once the summary is read (or failed for a reason other than 403), so an operator who may not read conflicts is never sent to the tab. Every read of the package's screens (a single value, or the first page of a list) is drawn as the operator may read it, as the console foundation settles it (keepLastGood): a 403 answer, to the first read or to a re-read of the same key, withholds the value it showed exactly as a first read answered 403, and the forbidden state holds through every later answer of that key that does not carry its value (a 503 after a 403 brings nothing back and is not announced again) until the read itself answers the value again or the key changes. Neither a write's reply nor a 最新を読み込んで続ける read brings a withheld value back, since neither tells anything about a 403 answered meanwhile. A later page carries no status in the console foundation (LoadMore's failure), so a later page answered 403 re-reads the list's first page, whose 403 then withholds the list the same way; the re-read discards the page's answer, so the 403 never shows as a load-more failure (whose sentence would state neither the refusal nor a cause). Props: host (Pick<PolicyConflictHost, "loadSummary">), knowledgeTabLabel, onOpenKnowledgeTab.
  14. PolicyConflictQueue: The policy-conflict card of the 「権威台帳 & 規程衝突」 tab: the detection band (last detection, next effective or end instant, 今すぐ検知 and its outcomes, the failed tie groups), the state and severity filters, and a master-detail of the keyset-paged list (each row with its severity and state pills) and the selected pair (both documents compared, the tie marked 同値, the recorded decision, and the decision form checked with the core's checkPolicyConflictDecision; a decision is a record of judgement and does not change citations). The list and the selected pair's detail are read only once the summary says the tenant was detected; a forbidden summary (its first read or a re-read answered 403, and every later failure until the summary is answered again) shows the forbidden sentence alone and withholds the band, the list and the detail it showed. Without a detection record the band says only that (規程衝突の検知の記録がありません。 with 今すぐ検知 to press, or when the counts and the list come), never that the tenant was never detected. When the first page answers that the conflicts are unmeasured (the detection record is gone), the list area says so (規程衝突の検知の記録がなくなったため、一覧を示せません。 with 今すぐ検知 to press, or when the list comes back) and the summary is re-read once. An empty list names every filter chosen (この状態の規程衝突はありません。, or この状態と重大度の規程衝突はありません。 under a severity), since the state's option shows the state's whole count. The decision region and 今すぐ検知 are drawn from the card's one write permission (the summary's viewer.canDecide; a 403 revokes it); until the summary tells it, the permission is unknown and the card says 書き込みの権限を確認しています。 instead of drawing a write region or a read-only variant of its notes. The decision form's two fields are 判断 (the verdict) and 理由 everywhere (the form, its errors, the summary of a sent or read-only decision, and the recorded decision, 判断: <the verdict as the form names it> · <who> · <when>), and a decision (裁定) records both. The stale note asks an operator who may detect to press the card's own 今すぐ検知, and tells any other operator that the counts and the list may differ from the ledger. A typed decision is a draft of its pair under the rules shared with DocumentAuthorityRegistry (below): it is kept when another pair is selected, when the detail is closed with Escape, or while the card is hidden, and the card lists it as held with 開く. A decision is sent with the facts of its draft's basis (expectedFactsHash); a STALE refusal keeps the verdict and the reason and offers 最新を読み込んで続ける, which re-reads the pair (ending the draft when the pair is already decided as typed, and drawing the typed decision read-only with only 破棄する when it was decided otherwise). A pair whose detail cannot be read is drawn from the draft's basis above the failure (a 403 withholds even its names and offers only この組の送っていない裁定を破棄する; a decision's answer or a 最新を読み込んで続ける read arriving after it does not bring the pair back but has the detail read the pair again, and only that read brings it back), so a typed decision can always be ended. A pair read answered 403 (the detail, or a 最新を読み込んで続ける read, also of a pair the detail does not show) re-reads the summary and the list, whose own answers then withdraw what the operator may no longer read (a held decision's entry included). 今すぐ検知 shows sending only while it waits and never waits for a decision. A detection answered failed, or one whose outcome is uncertain, re-reads the summary, the list and the open detail in place; a decision whose outcome is uncertain re-reads the summary and the pair's open detail. Props: host (PolicyConflictHost), revision (re-reads the summary, the first page and the open detail without clearing them).
  15. DocumentAuthorityRegistry: The document authority ledger: the keyset-paged ledger with its state and category filters, a row's operations (edit, approve, delete, withdraw, deprecate; which ones a row admits is decided by the core's documentAuthorityCommandBlocker), each confirmed with its effect on the citations of AI answers, and the registration of an unregistered document version with the approved row it replaces (a radio group; the searchable picker of replaceable rows opens only from 置き換え対象を選ぶ, for the category typed then). The document a version replaces has one name everywhere, 置き換え対象 (the field 置き換え対象の文書, a row's 置き換え対象: 「…」 beside its 後継: 「…」, the refusals and errors), and the version itself is always この版, so the direction of an irreversible replacement never reads the other way round. Every status is shown with what it means for the citations: the status filter's description names all three (下書き 承認までこの版は AI 回答の引用に使われません。, 承認済み 発効日時から失効日時まで AI 回答の引用の根拠になります。, 廃止 この版は AI 回答の引用に使われません。), a row's operations section starts with 状態: <name>。<meaning> (except for a row a draft found gone, below), and a registration's success says the draft is not cited until approved. Commands are checked with the core's checkDocumentAuthorityCommand before sending; instants are typed as wall-clock times of the draft's time zone (the root's when the draft was opened, named beside each instant field), and a recorded instant the field cannot show (a fraction of a second) is drawn as an empty field with the recorded value in its description and is never sent back as an empty field. A row's edit (編集…), a row's confirmation (承認…, 削除…, 承認の取消…, 廃止…) and a registration are drafts of their subject, under the rules shared with PolicyConflictQueue (below). An unchanged edit cannot be saved (変えた欄がありません。); an edit holding input makes the row's other openers wait (編集を保存するか破棄すると<command>できます。), and 編集… swaps an open confirmation for the form. Changing the category drops the chosen replacement and announces it. The open row's section stays open across filter changes and re-reads, drawn from the list's row while the list holds it and otherwise from the newest row the card read while it was open (the row as the list last showed it, as it was opened, or as an answer or a re-read last carried it), with この行は今の一覧に表示されていません。. A held draft's 開く keeps that row when the section is open on the draft's row. A row the list showed while no section was open is not kept: otherwise the held 開く opens on the list's row, else on the row the draft's current answer carried (STALE, DOCUMENT_NOT_CURRENT), else on the draft's basis row (a draft whose 最新を読み込んで続ける read failed carries no row, so it opens there too; a command sent from the basis row is refused as stale and 最新を読み込んで続ける reads the current row). Only 閉じる, 操作…, Escape, the row's command success, or ending the draft of a row found gone (NOT_FOUND, or a 最新を読み込んで続ける read that found no row) close it; while that draft is shown the section draws only the draft's own command, and no status, other opener or blocker sentence of a row that no longer exists. A press in the same frame as an answer never draws an older row back into the section nor reopens a section the answer closed (the section's controls change only whether its editor is open). A registration is sent in one step (下書きで登録する, with the confirmation sentence beside it); a candidate's 選ぶ (and a held registration's 開く) moves focus into the registration region, to its document line naming the chosen document (the region is drawn after the candidates and their LoadMore; the line comes first, so reading on from it passes the draft's notice before the form); ALREADY_REGISTERED keeps the typed values read-only with only 破棄する, and after a registration's own success its region shows 「<doc>」を下書きで登録しました。 until another candidate is chosen or it is closed. The card's detection note (authority-detection-notice, or authority-detection-alert for a failed detection) names the document: it is replaced by every success of a command that runs a detection (the core's DOCUMENT_AUTHORITY_COMMAND_RULES[kind].runsDetection: approve, withdraw, deprecate), cleared by a success that runs none or by 最新を読み込んで続ける that leaves nothing to send, and drawn only while the card may write. An operator who may not write the ledger (canUpdate false; the first render and a server render already know it) sees the ledger and its filters only (no operations column, operations section, registration or held drafts; the rows take the 32 px row height, since no cell holds a bordered control) with 権威台帳を編集する権限がありません。; a ledger read answered 403, the first read or a re-read, leaves only 権威台帳を閲覧する権限がありません。 until the ledger is answered again. Drafts are hidden meanwhile, never discarded. Props: host (DocumentAuthorityHost), canUpdate (whether the operator may write the ledger: the same capability the host reports as PolicyConflictSummary.viewer.canDecide, i.e. knowledge:update granted without a row or column scope, so the ledger and the queue never disagree), onPolicyConflictsChanged (called once for every command that runs a detection whose answer is a success, whatever the detection's outcome, or uncertain, also when the answer arrives after the card was hidden or unmounted).
  16. FilePasswordBoard: The passwords a tenant keeps for the protected files it receives (ZIP, PDF, Office), on the console foundation. Props: listing (FilePasswordListing: { kind: "listed", passwords } in the order they are tried, { kind: "forbidden" } for an operator who may not read them, or { kind: "unavailable", status } for a list the host could not read), busy (a submission of the host is in flight: the board sends nothing, so it never replaces it), and onSave(draft), onDelete(passwordId) and onMatch(facts), promises the host settles and rejects with the sentence of its refusal.
    • The table (file-password-table, rows file-password-row with data-password-id and data-active) writes each row's label (停止中 for an inactive row), its conditions one per line (the pattern syntax only beside a written pattern, すべてのファイル without any condition), its priority, how many files its password opened and when it last did, and its last write. The fixed columns' widths derive from the texts they hold (FILE_PASSWORD_COLUMN_PX); the flexible ones are never narrower than an instant.
    • The form (file-password-editor, a named region) adds a row (パスワードを追加) or rewrites one (編集). Its password field is a PasswordInput (new-password), required for a new row and kept when left empty on a rewrite (password: null). The draft (FilePasswordDraft) carries the texts as entered and an empty condition as null; the contract's bounds (widths in UTF-16 code units as the columns hold them (storedWidth), the password's UTF-8 bytes, a priority from 0 to FILE_PASSWORD_PRIORITY_MAX in ASCII digits) are checked at their fields before anything is sent, and focus moves to the first invalid field. A regular expression is checked by the host. While a form's values differ from its opening ones, or a save or a deletion is in flight, the other forms and the deletion stay closed, so typed values are never discarded silently; the reason names what holds them (EDITING_REASON for changed values, BUSY_REASON while an answer is awaited).
    • A deletion opens a confirmation region (file-password-delete) that says the password is no longer tried and the records of the files it opened stay.
    • The trial (file-password-match) takes a file's facts (its URI, and its file name, sender and subject when it has them) and lists the rows it would be tried with, in order, naming the rows a regular expression ran past its time on (in the warning and in the announcement alike).
    • A refusal is drawn above the form, confirmation or trial that sent it (AlertMessage, read once per refusal) with the entered values kept; a save, a deletion and a trial's answer are announced once. Closing the form discards the typed password, and nothing the board draws or announces carries one.

Rules shared by PolicyConflictQueue and DocumentAuthorityRegistry (one write model; its modules are internal and not exported):

  • Drafts. Each subject (a ledger row, a registration of one document version, a pair) holds at most one draft: the values typed or the confirmation opened, with the basis the operator acted on (the row or pair shown, and the time zone of the wall times typed). The server is the only version guard: a command carries the basis's version, and a stale one is refused. Escape, 閉じる, closing a section, selecting another row, candidate or pair, a filter change, a re-read, a refusal, a permission loss and hiding the card keep a draft that holds input; a draft ends only by 破棄する, its command's success, or a re-read that leaves nothing to send.
  • Sending. The fields are replaced by the values sent while the command is in flight, and the submit control is busy and keeps focus; the commands of different subjects and 今すぐ検知 may be in flight together, while a subject's other openers wait, each naming its own command and promising nothing about the answer, which may change the row's status (送った命令の答えを待つ間は<command>できません。, 最新の内容を読み込む間は<command>できません。 while the subject is re-read), and the controls that would end the draft wait too (送った命令の答えを待っています。; the decision card names its write 裁定).
  • Answers. An answer is applied to its subject wherever the operator went (the lists and detail shown on arrival patched, the host notified) and announced once, naming the subject (「<doc>」を承認しました。, 「<subject>」の<command>: <sentence>); it moves focus only when focus is inside the subject (its region, its list row or its held entry). A coded refusal keeps the typed values with one fixed sentence: a field error returns to the form, a stale version offers 最新を読み込んで続ける, a gone subject offers only 破棄する (a confirmation: 閉じる), the rest may be sent again. A client error says 命令は受け付けられませんでした (<reason>)。 (a decision: 裁定は受け付けられませんでした (<reason>)。; an empty reason leaves no parentheses); a 403 states the refusal, 命令は拒まれました。 (裁定は拒まれました。, 検知は拒まれました。), followed by the cause the route declared as a second sentence (…拒まれました。<reason>。, the refusal alone for ""); an uncertain outcome keeps the values and offers 最新を読み込んで続ける (a registration may simply be sent again: its unique key refuses a duplicate). After the card was unmounted, an answer is announced as a fact only (…は記録されませんでした。, …の結果を確かめられませんでした (…)。, with 入力した内容は破棄済みです。 when values were typed).
  • Continuing. 最新を読み込んで続ける re-reads the subject (a ledger row by loadPage({after: id + 1, limit: 1}), a pair by loadDetail): nothing left to send ends the draft (最新の内容は既に送ろうとした結果になっています。送るものはありません。, whatever the command), a gone subject ends with only 破棄する, and otherwise the draft takes the fresh basis, keeping every unit the operator changed and naming the units others changed (他の操作で変わった欄: …); a re-read that fails says 最新の内容を読み込めませんでした (<reason>)。, and one answered 403 最新の内容の読み込みは拒まれました。 followed by the declared cause (the refusal alone for ""); a draft that remains is announced with its region's own notice sentence (when the fresh basis no longer admits the command, the blocked sentence: why it cannot be sent and what to do), or 最新の内容を読み込みました。 when the notice has nothing to say. Focus goes to the first field, to a confirmation's sentence, or to 破棄する when the fresh basis leaves the form read-only (the pressed 最新を読み込んで続ける is gone; focus never falls to <body>).
  • Held drafts. A draft whose region is not drawn is listed on the card with a sentence true for it (「<subject>」に送っていない入力があります。, …を送っています。, …の最新の内容を読み込んでいます。, …の結果を確かめられませんでした。 after an uncertain outcome, …の最新の内容を読み込めませんでした。 after a failed re-read (which may follow an uncertain outcome or a refusal; …の最新の内容の読み込みは拒まれました。 for a 403), and otherwise …は記録されていません。開いて内容を確かめてください。) and 開く, in a warning notice like the region's (each entry is an unfinished write), entries spaced wider than an entry's notice and its controls; a value-less one also has 閉じる, unavailable while it is in flight. A value-less draft holds no input, so a confirmation that a re-read reopened is listed with …は記録されていません。…. Operator-typed text (a decision's reason, a value drawn read-only) keeps its line breaks wherever it is drawn.
  • Lists while typing. A keystroke changes only its draft, so the ledger's, the candidates' and the conflict lists, which take only values that keep their identity while the operator types, are not drawn again for it.
  • Keyboard. Enter sends only from a value field of a form (a text, datetime-local or password input inside the form's fields that owns no suggestion list; FilePasswordBoard's forms follow the same rule), never while composing (IME) or after another handler prevented it; Enter in a field with a suggestion list (規程区分's known categories) is left to the browser's list. The forms are no native <form> and their submit controls are plain buttons, so the browser's implicit submission has no path to a command. Escape in a form closes it and keeps the values; in an open confirmation it is やめる while the confirmation may be sent, and 閉じる once it was sent (a sent command cannot be cancelled) or while the row no longer admits the command, as its notice says; these words are the package's own, whatever the root's language, because the sentences quote them. Focus never falls to <body>: the targets are explicit, a write region that disappears hands focus to the card's heading, and so does what a read answered 403 withdraws (the candidates and their registration hand it to the registration's heading, the replacement picker to its own heading).

The host function types IntegrityFindingsLoader, PolicyConflictHost and DocumentAuthorityHost take the abort signal last and answer with the foundation's ResourceOutcome (reads) or CommandOutcome (the decision, the detection and the ledger commands, whose failure carries the host-validated rejection body of @aiquants/enrichment-governance-core, or null). A host passes the signal to its transport. A shown read carries the foundation's own signal, which aborts a read no longer shown; the cards' commands and the re-reads of 最新を読み込んで続ける (loadPage({after: id + 1, limit: 1}), absent unless the first row is the subject; loadDetail, 404 gone and 403 unreadable) carry a signal that is never aborted, since a card cannot tell being hidden from being unmounted. A card keeps one draft per subject, and the commands of different subjects and 今すぐ検知 fly together. An answer is applied wherever the operator is, notified and named once. A host function that breaks its contract (a rejected promise, or a synchronous throw) reaches reportError, and the call still settles in the card (a command as an uncertain answer, a re-read as a failed read, both with the reason ホストの応答が約束に反しました). The host's stale refusal is the only version guard. A failure's reason follows the foundation's contract (ResourceOutcome): for a 403 it is the cause the route declared, a phrase without a closing full stop that names what the operator must fix and states no refusal, or "" when the route declared none. A card draws a read answered 403 as its own fixed refusal followed by that cause as a second sentence (規程衝突を閲覧する権限がありません。<cause>。, the fixed sentence alone for ""), so a cause that restated the refusal would show it twice. A command or a 最新を読み込んで続ける read answered 403 is stated the same way (the refusal, then the cause); every other sentence that places a reason in parentheses ((<reason>)) leaves an empty one out.

Card Titles and Colour Contrast

  • Every card title (h2) is 16 px bold (fontSize: "16px", fontWeight: 700, or the console foundation's title type step for the cards built on it) — the same scale @aiquants/enrichment-react and @aiquants/enrichment-react-router use, so sibling cards on one console tab read as one heading level (tests/HeadingOutline.spec.tsx).
  • The cards built on the console foundation are scanned with its colour-role oracle scanColourRoles (@aiquants/console-ui/testing) in the states that draw coloured text, so every text sits on a pair of the foundation's palette table, which meets WCAG 2 AAA in both colour schemes (tests/colorContrast.spec.tsx), and their sources hold no inline style, className or colour literal (tests/migratedSources.spec.ts).
  • The panels not yet moved onto the foundation (CitationProofBadge, ClassificationRuleEditor, ConfidentialityPolicyBoard, FeatureFlagBoard, GovernanceDryRunConsole, GraphNeighborhoodMap, GraphNodeIndex, GraphTopologyMap, InteractiveGraphCanvas, KnowledgeGraphExplorer, PromptTemplateConsole, SystemParameterTable, TranscriptionAdaptationBoard) still style with inline colours, which do not reach AAA. 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; its colours are not measured until the stage that moves it onto the colour roles.
  • The topology adjacency table (topology-adjacency) lets relationship names wrap anywhere, so an unbroken name cannot widen the table past a narrow card.
  • Every select draws its own border (1px solid #cbd5e1, 4 px radius), so it still reads as a control under a host that resets borders (* { border: none }). The policy grid's per-cell prompt-row select (policy-prompt-<level>-<capability>) also takes 12 px text and 4 px 8 px padding at the full width of its column; tests/ConfidentialityPolicyBoard.spec.tsx pins that select's border, radius, padding, text size and width.

Interactive Graph Figures

Both graph figures in KnowledgeGraphExplorer are drawn on InteractiveGraphCanvas:

  • Drag the background to pan; wheel to zoom about the cursor (factor 1.1 per 100 px of wheel delta, scale clamped to [0.25, 4]); drag a node to move it (its edges are re-routed from the live positions); hover a node to highlight its incident edges (data-highlighted="true") and dim the rest; press and release without moving (< 3 px) to select.
  • Wheel deltas are normalised by deltaMode before they become a zoom factor: pixels as reported, a line as WHEEL_LINE_HEIGHT_PX (16) px, a page as the browser viewport's height (window.innerHeight); any other mode throws. A line-mode wheel (Firefox on Windows / Linux) therefore zooms like a pixel-mode one.
  • Touch keeps the page scrollable over the figure: the <svg> has touch-action: pan-y pinch-zoom, so a vertical swipe scrolls the page and a pinch zooms the page, while a horizontal swipe pans the figure or drags a node. Mouse and pen are unaffected (a drag in any direction pans or moves a node); the toolbar zooms the figure on every device.
  • A pan or a node drag writes state at most once per animation frame (requestAnimationFrame): pointer moves within one frame overwrite the pending move and the frame applies the last one; releasing or cancelling the press applies a pending move at once; unmounting cancels the pending frame.
  • The figure stays mounted when its data changes (nodes added, removed or replaced). The canvas re-seeds only when the nodes' ids, order or coordinates change, and applies the re-seed within the render that brings the new nodes, so every node has a position in that render (reseedPositions):
    • a node whose seed point is unchanged keeps where the operator dragged it; a new node, or one the layout moved, stands on its new coordinates; removed nodes are dropped;
    • the same nodes redrawn in the same order keep every drag and the viewport; a reorder alone keeps every drag (no seed point moved) but returns the viewport to the whole figure, as any other re-seed does.
  • Toolbar buttons <prefix>-zoom-in, <prefix>-zoom-out, <prefix>-reset-view and the scale readout <prefix>-scale (e.g. 100%) are the keyboard-reachable part; the <svg data-testid="<prefix>-svg"> itself is aria-hidden, so every figure is accompanied by a table carrying the same content.
  • DOM hooks are data-* only: <prefix>-node-<id> (data-node-id, data-selected, data-hovered, plus any data the caller supplies) and <prefix>-edge-<id> (data-edge-id, data-highlighted).

InteractiveGraphCanvas props: size, nodes ({ id, label, title?, x, y, radius, fill?, stroke?, data? }), edges ({ id, from, to, label?, width?, data? }), edgePath(from, to, edge) (the caller supplies the geometry — chordPath / selfLoopPath from the topology layout and routeNeighborhoodEdge / straightPath from the neighbourhood layout are exported for this), selectedId?, onSelect?(id), ariaLabel, testIdPrefix. An edge whose endpoint is not among nodes throws rather than being dropped. A node's label is drawn under it with the metrics of GRAPH_LABEL (font size 11, baseline 12 below the rim); title, when given, becomes the node's <title> tooltip (the place for the full text of a shortened label). The SVG scales down with a narrow frame but never past its natural size (maxWidth = size px, height following the square figure), and a frame wider than that centres it rather than leaving the figure on one side.

Label helpers (graphLabel): GRAPH_LABEL (fontSize, baselineGap, clearance 4), labelTextWidth(text) (fixed width model: a full-width glyph is 1 em, printable ASCII and half-width forms 0.55 em — the same model as the enrichment console's pipeline figure), fitLabel(text, maxWidth) (the text itself when it fits, else its longest fitting prefix followed by LABEL_ELLIPSIS …, else an empty string) and labelBand(y, radius) (the vertical extent of the label: one em above the baseline, a quarter em below).

GraphTopologyMap (inside KnowledgeGraphExplorer, prefix topology) starts from the deterministic ring layout (layoutTopology) and returns to it with "配置を戻す"; a node click calls onSelectLabel exactly like the table's topology-select-<label> button.

GraphNeighborhoodMap (start: { canonicalId, label }, rows, selectedCanonicalId?, onSelectNode?(canonicalId); prefix neighborhood) places the rows a traversal runner returned with layoutNeighborhood(start, rows, geometry?):

  • A node is named by its label and its identifier together: neighborhoodNodeKey(label, canonicalId) percent-encodes both and joins them with / (Entity/E-1, File/gs%3A%2F%2Fb%2Fa.pdf), or is the encoded identifier alone for a row that carries no label. An entity's canonical ID and a document's URI are separate namespaces, so an Entity and a File with the same identifier are two nodes. NeighborhoodNode.id, the edges' from / to, parentId and every test id use this key; canonicalId and label carry what the rows said. The start must carry the label its rows give it (a traversal from an entity: label: "Entity"); with another label the node the rows call the start is a different node from the one at the centre.
  • Row shapes read (readTraversalRow): a reached node { canonical_id, label, name, hops } — placed on ring hops; a hops of 1 certifies a direct relationship with the start, drawn with type: null — unless a path step names a typed relationship between the start and that node (either direction), in which case only the typed edge is drawn and listed (it is the same relationship; the untyped copy would lie on the same line). A path step { from_id, from_label, from_name, relation, to_id, to_label, to_name } — an edge; hop distances follow the edges from the start (BFS). A node's ring is the smaller of its certified and traversed distance. A label or name that is not a string reads as null.
  • A node's parentId (the fan its slice is cut from) is the nearest node on its traced path back to the start that lies on a strictly inner ring — the start for a node reached by distance only. A node whose certified hop is shorter than its traced depth therefore gets its own slice on its certified ring instead of sitting on its traced parent: no two nodes share a position and no edge between distinct nodes has zero length.
  • Angles are allotted recursively: each subtree receives a span proportional to its leaf count, centred on its parent's angle; the start sits at the centre. Identical rows give identical coordinates (rounded to 3 decimals, -0 normalised).
  • Ring radii grow with crowding: ring k lies at least ringGap beyond ring k−1, and further out when its two closest neighbours (the smallest angular gap on the ring, wrap-around included) would stand closer than a node diameter plus labelGap (NEIGHBORHOOD_GEOMETRY: ringGap 120, nodeRadius 14, labelGap 16, padding 24, minimumSize 320). A ring that edges span (one endpoint inside it, one outside) also keeps room for their crossings: with D(k) the number of edges spanning ring k, m its nodes, c = 22 and δ = 2·asin((nodeRadius / 2) / 2R), a crossing avoids at most E = D(k−1) + D(k) + D(k+1) + 1 angles, and when 2δ·E > 2π − 2m·asin(c / R) the ring moves out to the smallest radius where it no longer is (outer rings follow). The figure's size follows the outermost ring, or the outermost lane of an edge routed outside it: max(minimumSize, 2 × (padding + max(nodeRadius + outermost radius, outermost lane radius))).
  • No edge passes through a node it does not connect. Each edge carries its route (NeighborhoodEdgeRoute) and its path (the route drawn with routeNeighborhoodEdge from the laid-out positions):
    • straight when the segment keeps at least nodeRadius + labelGap / 2 (22) from the centre of every node it does not connect and runs along no arc of a relationship joining a different pair (below); loop for a relationship back to its own node (an outward loop).
    • rings otherwise — e.g. a chord between two nodes of one ring that would run under the nodes between them or through the start (diametrically opposite nodes), a relationship between distant slices, or a segment that would run along another relationship's arc. The path runs in the bands between rings (which hold no node): two nodes of one ring use the band just outside it; nodes on different rings use every band between them. It follows each band on a lane (an arc concentric with the start) turning through the signed angle in turns (positive = increasing angle), crosses a ring radially at the angle in crossings, and joins each endpoint with a leg parallel to that endpoint's radius, moved sideways by shifts ([from, to]). Lanes keep 22 from both rings of their band (the band outside the outermost ring counts as ringGap wide); arcs in one band whose angular ranges overlap, ends included (compared within 1e-9 rad, so two arcs meeting at a node's angle never share a lane), get different lanes, assigned in edge order and spread evenly across the band ((width − 2 × 22) / (n + 1) apart for n lanes; nothing widens a band for its lanes).
    • Crossings: in edge order, each path crosses a ring at the free angle nearest its target — open (a ring node at radius R blocks the radii within asin(22 / R) of its angle) and at least half a node radius (at the ring) from the crossings already placed on that ring and the neighbouring rings, from straight edges lying on a radius across the ring, and from the path's own endpoints on the neighbouring rings.
    • Leg shifts: always towards the arc, never zero, at most half the node radius and at most the chord of a quarter of the arc. Legs of different edges clash when they would lie on one line — the same node on the same side, or nodes at the same angle into the same band over a common stretch; a leg with n − 1 clashing legs takes the largest of k · cap / n (k = n … 1) that stays half a step from every clashing leg placed before it, so a leg without clashes keeps its whole cap (half the node radius, e.g. two legs leaving one node on one side get 7 and 3.5).
    • Straight edges and arcs: a straight edge never runs along an arc of a relationship joining a different pair — wherever it passes inside the arc's circle, or less than half a node radius (7) outside it, at an angle the arc covers (ends included), its line meets that circle at 15° or more (two lines crossing at 15° stay within half a stroke width of each other over less than four stroke widths). The layout first decides straight vs rings by node clearance, plans the rings, then moves every straight edge that would meet an arc more shallowly onto the rings and plans again, until none is left (edges only move from straight to the rings, so this ends).
    • What holds between two relationships that join different pairs of nodes: no two straight pieces (straight edges, legs, crossings) lie on one line over a common stretch — two straight edges cannot without passing over a node; a leg's line passes its node off-centre, so no straight edge lies on it; legs lie on no line through the centre and clashing legs get different shifts; crossings are radial, stand apart as above, and the only other straight edges on a line through the centre are the start's own edges inside ring 1, where no path crosses. A straight edge never runs along an arc (above). Arcs overlap only on different lanes and are drawn turning the planned way — a half turn between opposite nodes is never re-decided from the drawn coordinates.
    • What is not excluded: two straight pieces may meet at a shallow angle (for example two straight edges that cross, or a straight edge leaving a node beside a leg of that node), and where many paths share one band or one node, lanes or leg shifts can stand closer than a stroke's width. Relationships between the same two nodes drawn straight lie on one line; the table lists each.
    • The map draws every edge with routeNeighborhoodEdge(from, to, route, center, geometry) from the live positions, so a dragged node keeps the same kind of path (lanes, crossing angles, turns and leg shifts stay; legs follow the node, and an arc keeps turning the planned way, past a half turn if the drag takes it there).
  • Every node carries labelWidth: the widest name that can be written centred under it without meeting another name (two names on overlapping lines each take at most half the horizontal room between their nodes less GRAPH_LABEL.clearance), another node whose circle reaches into its line (stopping clearance short of it), or the figure's left or right edge. The clearance is sideways only, so the names on the sides of a crowded ring, where nodes stand one above the other, are not emptied. The map draws fitLabel(name, labelWidth) — the name, or its identifier (canonicalId) when the rows carried no name — and puts the full text, followed by the label in parentheses when the rows carried one, in the node's <title>; the table below carries every full name. Widths are computed for the laid-out spot: a node the operator drags keeps its label.
  • Rows the layout cannot place — unreadable shapes, or path steps with an endpoint that no row places at a distance from the start — are counted, never guessed: neighborhood-unplaceable reports the number, and neighborhood-empty (起点の周りに描ける到達ノードはありません。) appears when nothing but the start remains.
  • The accessible table (neighborhood-table, rows neighborhood-row-<key> with data-selected / data-startable; edges in neighborhood-edge-table, rows neighborhood-adjacency-<edgeId>, whose endpoints are written as the rows' identifiers) carries the same nodes and edges. The node table's columns are ノード / 種類 (the label, neighborhood-label-<key>) / 名称 / 深さ — the distance from the start is called 深さ, as in the traversal form.
  • Choosing a node offers it as the next start, so only nodes of the start's label can be chosen: with onSelectNode such a node's identifier is a button (neighborhood-select-<key>) and a click on its circle calls onSelectNode(canonicalId); a node of another label has no button, ignores clicks, and is drawn in #f1f5f9 with a #64748b rim (data-startable="false", data-label). selectedCanonicalId marks the node of the start's label with that identifier.

useGraphViewport(initial, size, options?) is the hook behind the canvas (viewport state, node positions, pointer handlers to spread on the <svg>, reset, zoomBy, wasDrag); its pure helpers clientToSvg, zoomAbout, wheelDeltaPixels (with WHEEL_DELTA_MODE and WHEEL_LINE_HEIGHT_PX), wheelFactor, reseedPositions(previousSeed, nextSeed, live), viewBoxOf and formatViewBox are exported for tests and custom figures. positions covers every id of the current initial in every render.

License

MIT