From 8c228e9f6f4bc555fd7a45fb34762bb2eaba7143 Mon Sep 17 00:00:00 2001 From: Kentaro Saida Date: Sat, 20 Jun 2026 13:24:31 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=BB=95=E6=A7=98=E3=83=89=E3=82=AD?= =?UTF-8?q?=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88=E3=82=92=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=E3=81=AB=E8=BF=BD=E5=BE=93=EF=BC=88=E6=95=B4=E5=90=88=E6=80=A7?= =?UTF-8?q?=E7=9B=A3=E6=9F=BB=E3=81=AE=E5=8F=8D=E6=98=A0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 実装とドキュメントの整合性監査で見つかった追従漏れを修正。コード変更なし (実装が正・docが古いものを doc 側で修正、および未実装項目をバックログ化)。 API: - api-spec: 未定義サブパスの 404 / 500 の imsx 変換が未実装である旨を明記し、 conformance backlog C14 / C15 として新規追加(実装済みのエラー形式も整理) - api-examples: rubric の weight/score を整数表記に修正(実装は整数値を int 化)、 エラー例の imsx_codeMinorFieldName を sourcedId に修正(実装は常に固定)、 500 例に「未実装・目標形」の注記を追加 Web UI (web-ui.md): - 逆参照「参照元(他機関)」セクションを追記(public 限定・private 除外の挙動含む) - を「ja 固定」から「リクエスト言語(Accept-Language、既定 en)」に修正 - フレームワーク一覧にルーブリック数列を追記 - /uri/ パンくずを実装どおり3段(所属ドキュメントへのリンク)に修正 CLI (cli.md): - tenant update の必須オプションエラー文言に --display-order / --clear-order を追加 - export case の出力説明を修正(GET と同一ペイロード、CLI は整形・API は compact) Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XXFogp18twGiPFBAZcqJw2 --- docs/dev/case-v1p1-conformance-backlog.md | 2 ++ docs/spec/api-examples.md | 19 ++++++++++++++++--- docs/spec/api-spec.md | 10 ++++++++-- docs/spec/cli.md | 8 ++++---- docs/spec/web-ui.md | 14 ++++++++------ 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/docs/dev/case-v1p1-conformance-backlog.md b/docs/dev/case-v1p1-conformance-backlog.md index c823d6a..a806f4e 100644 --- a/docs/dev/case-v1p1-conformance-backlog.md +++ b/docs/dev/case-v1p1-conformance-backlog.md @@ -34,6 +34,8 @@ compeito の現在のゴールは **OpenCASE / OpenSALT との実用的な相互 | C11 | **エラー封筒の `imsx_codeMinorFieldName`** | 常に既定の `"sourcedId"`。invalid_sort_field / invalid_selection_field 系では `sort` / `fields` / `limit` 等の実フィールド名が意味的に正しい | P3 | `imsx_error_response` に fieldName 引数を追加し、各呼び出し箇所で該当フィールド名を渡す | | C12 | **`ext:` associationType の文字種** | import 受理が `startswith("ext:")` のみで、公式パターン `(ext:)[a-zA-Z0-9.\-_]+` の文字種を検証しない(`ext:日本語` 等も通る) | P3 | 正規表現で検証し、不一致は invalid associationType として skip + warning | | C13 | **スキーマ層の出力時検証なし** | Pydantic スキーマで identifier の UUID パターン・associationType / targetType の enum を検証していない(import 側で防いでいるため実害は低い) | P3 | strict 出力モード導入時に field_validator で同梱 | +| C14 | **未定義サブパスの 404 が imsx 形式でない** | `/{tenant}/ims/case/v1p1/...` 配下の未定義サブパスは FastAPI/Starlette 既定の 404(`{"detail":"Not Found"}`)を返し、imsx_StatusInfo 形式になっていない(既知リソース種別で ID 不在の 404 `unknownobject` は実装済み) | P2 | CASE API パス配下の catch-all ルートまたは `StarletteHTTPException` ハンドラを `main.py` に追加し、imsx 404 に変換 | +| C15 | **500 が imsx 形式でない** | 未捕捉例外は Starlette 既定のプレーン 500 を返し、`internal_server_error` の imsx_StatusInfo 形式になっていない | P2 | グローバル `Exception` ハンドラを `main.py` に追加し、CASE API パスの 500 を imsx 形式に変換 | ## デプロイ上の制約(参考) diff --git a/docs/spec/api-examples.md b/docs/spec/api-examples.md index adc927e..a50b93a 100644 --- a/docs/spec/api-examples.md +++ b/docs/spec/api-examples.md @@ -535,7 +535,7 @@ GET /550e8400-e29b-41d4-a716-446655440000/ims/case/v1p1/CFRubrics?doc=d86774f2-1 "identifier": "itm11111-1111-1111-1111-111111111111", "uri": "https://case.example.com/550e8400-.../uri/itm11111-1111-1111-1111-111111111111" }, - "weight": 1.0, + "weight": 1, "position": 1, "rubricId": "rub11111-1111-1111-1111-111111111111", "lastChangeDateTime": "2025-04-01T00:00:00+09:00", @@ -545,7 +545,7 @@ GET /550e8400-e29b-41d4-a716-446655440000/ims/case/v1p1/CFRubrics?doc=d86774f2-1 "uri": "https://case.example.com/550e8400-.../uri/lvl11111-1111-1111-1111-111111111111", "description": "十分に理解している", "quality": "A", - "score": 5.0, + "score": 5, "feedback": "優れた理解を示しています", "position": 1, "rubricCriterionId": "cri11111-1111-1111-1111-111111111111", @@ -559,6 +559,11 @@ GET /550e8400-e29b-41d4-a716-446655440000/ims/case/v1p1/CFRubrics?doc=d86774f2-1 } ``` +> **Note on `weight` / `score`:** whole-number values are emitted as integers +> (`1`, not `1.0`; `5`, not `5.0`). The `CASEBaseSchema.serialize_int_or_float` +> serializer renders integer-valued floats as `int` for round-trip parity with +> OpenCASE (see [round_trip_status.md](../dev/round_trip_status.md) cat C). + **Response (200) — no rubrics:** ```json { @@ -578,13 +583,17 @@ GET /550e8400-.../ims/case/v1p1/CFRubrics "imsx_codeMinor": { "imsx_codeMinorField": [ { - "imsx_codeMinorFieldName": "ims.case.v1p1", + "imsx_codeMinorFieldName": "sourcedId", "imsx_codeMinorFieldValue": "invalid_selection_field" } ] } } ``` +> `imsx_codeMinorFieldName` is always `"sourcedId"` (per the imsx convention; see +> [api-spec.md](api-spec.md) error format and [conformance backlog](../dev/case-v1p1-conformance-backlog.md) C11). +> `imsx_description` carries the validation detail string from the framework, so the +> exact text varies; `"Validation error"` above is illustrative. **Error — `doc` is not a valid UUID (400):** ``` @@ -738,6 +747,10 @@ The response includes an `Allow: GET` header. ### 500 Internal Server Error +> ⚠️ **Target shape, not yet implemented.** Uncaught errors currently return +> Starlette's default plain 500, not the imsx shape below. Tracked as +> [conformance backlog](../dev/case-v1p1-conformance-backlog.md) C15. + ```json { "imsx_codeMajor": "failure", diff --git a/docs/spec/api-spec.md b/docs/spec/api-spec.md index b41b3a1..c126e71 100644 --- a/docs/spec/api-spec.md +++ b/docs/spec/api-spec.md @@ -254,7 +254,10 @@ The CASE v1.1 `imsx_StatusInfo` shape. Fields are at the root (no wrapper): - HTTP status mapping: 400 → `failure/error`; 404 → `failure/error` + `unknownobject`; 405 → `failure/error` + `invalid_selection_field`; 429 → `failure/error` + `server_busy`; 500 → `failure/error` + `internal_server_error`. - **429 (Server Busy):** defined for every endpoint in the CASE v1.1 OpenAPI. We do not implement rate limiting in Phase 1 explicitly, but API Gateway / Lambda throttling may yield 429. In that case we return the `server_busy` imsx_StatusInfo shape. - We do not use FastAPI's default 422 Validation Error; a custom exception handler converts `RequestValidationError` into a **400** `invalid_selection_field` imsx_StatusInfo response. -- Requests to undefined sub-paths under `/{tenant}/ims/case/v1p1/...` return **404** (`unknownobject`) in the imsx_StatusInfo shape. FastAPI/Starlette's default 404 isn't in imsx form, so a catch-all route or a custom handler for the CASE API path translates it. +- ⚠️ **Not yet implemented** (see [conformance backlog](../dev/case-v1p1-conformance-backlog.md) C14 / C15) — two cases in the status mapping above are not yet wired: + - **404 for undefined sub-paths:** requests to undefined sub-paths under `/{tenant}/ims/case/v1p1/...` currently fall through to FastAPI/Starlette's default 404 (`{"detail": "Not Found"}`), **not** the imsx_StatusInfo shape. A catch-all route or a `StarletteHTTPException` handler for the CASE API path is needed to translate it (C14). + - **500 (internal_server_error):** uncaught server errors currently return Starlette's default plain 500, **not** the `internal_server_error` imsx shape. A global exception handler is needed (C15). + - Error shapes that **are** implemented: 400 (`invalid_uuid` / `invalid_selection_field`), 404 `unknownobject` for a known resource type whose ID does not exist, and 405. ## Unsupported HTTP methods @@ -592,7 +595,10 @@ CASE v1.1 の imsx_StatusInfo 形式。ルートレベルに直接フィール - HTTPステータスコード対応: 400→`failure/error`, 404→`failure/error`+`unknownobject`, 405→`failure/error`+`invalid_selection_field`, 429→`failure/error`+`server_busy`, 500→`failure/error`+`internal_server_error` - **429 (Server Busy):** CASE v1.1 OpenAPI で全エンドポイントに定義されている。Phase 1 では明示的なレート制限を実装しないが、API Gateway / Lambda のスロットリングにより 429 が返される可能性がある。その場合は imsx_StatusInfo 形式で `server_busy` を返す - FastAPI のデフォルト 422 Validation Error レスポンスは使用せず、imsx_StatusInfo 形式の **400**(`invalid_selection_field`)に変換する。カスタム例外ハンドラで RequestValidationError をキャッチし、imsx_StatusInfo 形式で返す -- CASE API パス配下(`/{tenant}/ims/case/v1p1/...`)の未定義サブパスへのアクセスには **404**(`unknownobject`)を imsx_StatusInfo 形式で返す。FastAPI/Starlette のデフォルト 404 レスポンスは imsx_StatusInfo 形式ではないため、CASE API パス配下の catch-all ルートまたはカスタム例外ハンドラで変換する +- ⚠️ **未実装**([conformance backlog](../dev/case-v1p1-conformance-backlog.md) C14 / C15 参照)— 上記マッピングのうち以下2件は未配線: + - **未定義サブパスの 404:** `/{tenant}/ims/case/v1p1/...` 配下の未定義サブパスは現状 FastAPI/Starlette のデフォルト 404(`{"detail": "Not Found"}`)にフォールスルーし、imsx_StatusInfo 形式**ではない**。CASE API パス配下の catch-all ルートまたは `StarletteHTTPException` ハンドラで変換する必要がある(C14)。 + - **500(internal_server_error):** 未捕捉のサーバーエラーは現状 Starlette のデフォルトのプレーン 500 を返し、`internal_server_error` の imsx 形式**ではない**。グローバル例外ハンドラが必要(C15)。 + - **実装済み**のエラー形式: 400(`invalid_uuid` / `invalid_selection_field`)、既知リソース種別で ID 不在時の 404 `unknownobject`、405。 ## 非対応HTTPメソッド diff --git a/docs/spec/cli.md b/docs/spec/cli.md index 52948e3..15968b3 100644 --- a/docs/spec/cli.md +++ b/docs/spec/cli.md @@ -95,7 +95,7 @@ uv run python cli.py export csv --tenant {uuid} --doc {doc-uuid} --file output.c # are included. isChildOf is NOT repeated in the CF Association sheet. uv run python cli.py export xlsx --tenant {uuid} --doc {doc-uuid} --file output.xlsx -# CASE CFPackage JSON export (output is byte-for-byte identical to GET /CFPackages/{id}) +# CASE CFPackage JSON export (same payload as GET /CFPackages/{id}; the CLI pretty-prints with indent, the API serves compact JSON) # Re-importable via `import case --file`, or feed-able to any CASE-conformant editor. uv run python cli.py export case --tenant {uuid} --doc {doc-uuid} --file output.json @@ -162,7 +162,7 @@ uv run python cli.py doc delete --tenant {uuid} --doc {doc-uuid} - `--file` is not readable (permissions, etc.) → exit ("Cannot read file: '{filepath}'", code 1) - `--file` output path is not writable (directory missing, permissions) → exit ("Cannot write file: '{filepath}'", code 1) - CSV import: file is not valid UTF-8 → exit ("CSV file is not valid UTF-8", code 1) -- `tenant update` with none of `--name` / `--private` / `--public` / `--slug` / `--clear-slug` → exit ("At least one of --name, --private, --public, --slug, or --clear-slug is required", code 1) +- `tenant update` with none of `--name` / `--private` / `--public` / `--slug` / `--clear-slug` / `--display-order` / `--clear-order` → exit ("At least one of --name, --private, --public, --slug, --clear-slug, --display-order, or --clear-order is required", code 1) ## CSV import defaults @@ -294,7 +294,7 @@ uv run python cli.py export csv --tenant {uuid} --doc {doc-uuid} --file output.c # isChildOf は CF Association シートには重複出力しない。 uv run python cli.py export xlsx --tenant {uuid} --doc {doc-uuid} --file output.xlsx -# CASE CFPackage JSON エクスポート(出力は GET /CFPackages/{id} と同一のバイト列) +# CASE CFPackage JSON エクスポート(内容は GET /CFPackages/{id} と同一。CLI は可読性のため indent 付き整形、API は compact JSON) # import case --file で再取り込みするか、任意の CASE 準拠エディタへ受け渡せる uv run python cli.py export case --tenant {uuid} --doc {doc-uuid} --file output.json @@ -361,7 +361,7 @@ uv run python cli.py doc delete --tenant {uuid} --doc {doc-uuid} - `--file` で指定したファイルが読み取れない(パーミッションエラー等) → エラー終了(「Cannot read file: '{filepath}'」、終了コード 1) - `--file` で指定した出力先に書き込めない(ディレクトリが存在しない、パーミッションエラー等) → エラー終了(「Cannot write file: '{filepath}'」、終了コード 1) - CSVインポート時、ファイルが UTF-8 としてデコードできない → エラー終了(「CSV file is not valid UTF-8」、終了コード 1) -- `tenant update` に `--name` / `--private` / `--public` / `--slug` / `--clear-slug` のいずれも指定されていない → エラー終了(「--name、--private、--public、--slug、--clear-slugのいずれかを指定してください」、終了コード 1) +- `tenant update` に `--name` / `--private` / `--public` / `--slug` / `--clear-slug` / `--display-order` / `--clear-order` のいずれも指定されていない → エラー終了(「--name、--private、--public、--slug、--clear-slug、--display-order、--clear-orderのいずれかを指定してください」、終了コード 1) ## CSVインポートのデフォルト動作 diff --git a/docs/spec/web-ui.md b/docs/spec/web-ui.md index 5e31671..3d9de01 100644 --- a/docs/spec/web-ui.md +++ b/docs/spec/web-ui.md @@ -29,7 +29,7 @@ | Path | Description | |------|-------------| | GET / | Public tenant list. Tenant names (private tenants hidden). Sort: `display_order ASC NULLS LAST, name ASC, id ASC`. Each name links to `/{tenant-uuid}/`. If there are no public tenants, show "No public tenants". | -| GET /{tenant-uuid}/ | Framework list: CFDocument title, lastChangeDateTime, item count (`SELECT COUNT(*) FROM cf_item WHERE cf_document_id = doc.id`). Sort: `display_order ASC NULLS LAST, title ASC, identifier ASC`. Each title links to `/{tenant-uuid}/cftree/doc/{doc-uuid}`. If no documents, show "No frameworks". | +| GET /{tenant-uuid}/ | Framework list: CFDocument title, lastChangeDateTime, item count (`SELECT COUNT(*) FROM cf_item WHERE cf_document_id = doc.id`), and rubric count (`COUNT` of CFRubrics for the document). Sort: `display_order ASC NULLS LAST, title ASC, identifier ASC`. Each title links to `/{tenant-uuid}/cftree/doc/{doc-uuid}`. If no documents, show "No frameworks". | | GET /{tenant-uuid}/cftree/doc/{doc-uuid} | Tree view. **Lazy tree** — initial SSR of depth 0-1 with native `
`; deeper levels load on expand via the `/children/` route. `?item={uuid}` (back-compat) SSRs the ancestor path to that item + its detail in the right pane. | | GET /{tenant-uuid}/cftree/doc/{doc-uuid}/children/{parent-uuid} | HTML fragment of one level of a parent item's children (lazy tree expand). Same `tree_nodes.html` markup as the initial SSR. | | GET /{tenant-uuid}/cftree/doc/{doc-uuid}/item/{item-uuid} | Same tree view with an item selected via the **URL path** — the canonical, shareable, reload-safe form that in-tree navigation pushes (`hx-push-url`). Opening/reloading/sharing reconstructs the tree (the ancestor path expanded to the item) + the item's full detail via SSR. | @@ -50,13 +50,13 @@ A two-pane layout inspired by OpenSALT's tree view. Visually, use a modern Tailw - `GET /{tenant}/uri/{uuid}`: depends on the resource type. CFItem → "{first 50 chars of fullStatement} - COMPEITO". CFDocument → "{title} - COMPEITO". Lookup / CFAssociation → "{title or identifier} - COMPEITO". - Error pages: "{status code} - COMPEITO". -**HTML ``**: `base.html` sets `lang="ja"` as a fixed value (the management UI is in Japanese). Even when a resource on a `/uri/` page has a `language` field, `` is not changed (content language is expressed by the resource's `language` field, not by the `lang` attribute). +**HTML ``**: `base.html` sets `lang` from the request language (`{{ lang|default('en') }}`), which is negotiated from the `Accept-Language` header (default `en`; see `i18n.parse_accept_language`). The UI itself is bilingual (en/ja). Note: this is the *UI chrome* language; the content language of a resource is expressed separately by the resource's own `language` field and does not drive ``. **Navigation:** every page shows a breadcrumb in the header: - `GET /`: no breadcrumb (top page). - `GET /{tenant}/`: "[Tenants](/)". - `GET /{tenant}/cftree/doc/{doc}`: "[Tenants](/) > [Tenant name](/{tenant}/) > Document title" (the last segment is the current page, no link). -- `GET /{tenant}/uri/{uuid}`: depends on the resource. CFItem / CFAssociation → "[Tenants](/) > [Tenant name](/{tenant}/)" (the owning document is shown via `CFDocumentURI` inside the page). CFDocument → same. Lookup → same. +- `GET /{tenant}/uri/{uuid}`: depends on the resource. For CFItem / CFAssociation / CFRubric the owning document is resolved, so a third segment links to it: "[Tenants](/) > [Tenant name](/{tenant}/) > [Document title](/{tenant}/cftree/doc/{doc})". CFDocument and lookups (no owning document) show the two-segment breadcrumb. The page body also shows the document via `CFDocumentURI`. ``` ┌─────────────────────────────────────────────────────┐ @@ -87,6 +87,7 @@ A two-pane layout inspired by OpenSALT's tree view. Visually, use a modern Tailw - **another tenant on this instance, public** → the relation is **owned by the declaring tenant**, but if a `*NodeURI` points at a CFItem in a *public* other tenant on this same compeito instance (a `{base_url}/{tenant}/uri/{item}` permalink — see `uri_service.parse_internal_tenant_id`), it is resolved to the target's title and linked to **that tenant's tree** (`/{other-tenant}/cftree/doc/{other-doc}/item/{id}`) with an "Other institution" badge (`related_other_institution`). Resolved server-side via `web._resolve_cross_tenant` → `tenant_service.get_tenant` (visibility check) + `cf_item_repository.map_identifiers_to_items` (`related_other_tenant` / `assoc_node_*` carry a `tenant_segment`). - **another tenant on this instance, private** → **fully hidden**: a private (or nonexistent) target tenant's endpoint is dropped entirely (no title, no URI, no link, no badge) — its existence is never surfaced. - **external / unresolvable** → link out to the stored URI in a new tab (`target=_blank`) with an "External" badge, **http(s) only** — other schemes (`javascript:` / `data:`) render as plain text. Internal `{base_url}` URIs that did not resolve to a public item are *not* linkified here (they fall into the private/hidden case above). + - **Incoming references ("Referenced by other institutions" / 参照元(他機関))**: the reverse direction of the public cross-tenant case above. A CFItem's pane lists CFAssociations *owned by other tenants* that point **at this item** as their destination — i.e. who on this instance has adopted/linked to this item. Resolved server-side by `web._incoming_refs`: it builds this item's permalink (`{base_url}/{tenant}/uri/{identifier}`), looks up incoming associations tenant-wide via `cf_association_repository.list_incoming_by_destination_uri` (excludes `isChildOf`), drops references from the current tenant, and — like the outgoing case — is **public-only**: associations owned by **private** tenants are excluded *before* resolution, so a private adopter is never surfaced (neither forward nor in reverse). Each entry shows the origin item's title with an "Other institution ↩" badge (`incoming_refs` / `related_other_institution_in`, `incoming_refs_label`) linking to that tenant's tree. - In-pane "back to the linked item" / parent links use **path-neutral labels** (`view_linked_item` "View linked item", `go_to_top_page`) so they read correctly however the node was reached. - **Cross-document hierarchy (上位/下位 別FW)**: a CFItem's pane shows "Parent (other framework)" / "Child (other framework)" sections when its `isChildOf` parents/children live in **another framework** (a large framework split across documents). The tree stays per-document; only the boundary neighbors surface here, each a tree-switch link (or external link-out). Same-document hierarchy is omitted (it's already in the tree). Resolved server-side from tenant-wide `isChildOf` (`cf_association_repository.list_ischildof_parents` / `_children` + `cf_item_repository.map_identifiers_to_items`, `_cross_doc_hierarchy`). isChildOf direction: origin=child, destination=parent. The same cross-tenant routing applies: a parent/child in a **public other tenant** links to that tenant's tree with the "Other institution" badge; one in a **private** other tenant is dropped (not shown). - **Right pane = full detail.** The pane renders the **same full-detail card as the standalone `/uri/{uuid}` page** (shared partial `fragments/resource_detail.html`), so every field — including the permalink, API URLs, effective license, and the grouped "Related" associations — is visible without leaving the tree. The right-pane fragment endpoint and the deep-link `?item=` SSR both produce this full card. (The previous lightweight summary + "Detail" link round-trip is replaced.) @@ -369,7 +370,7 @@ The `uri` field of CASE resources points at `/uri/{uuid}` (same pattern as OpenS | Path | 説明 | |------|------| | GET / | 公開テナント一覧: テナント名の一覧(privateは非表示)。`display_order ASC NULLS LAST, name ASC, id ASC` でソート。各テナント名は `/{tenant-uuid}/` へのリンク。公開テナントが0件の場合は「公開テナントはありません」を表示 | -| GET /{tenant-uuid}/ | フレームワーク一覧: CFDocumentのtitle, lastChangeDateTime, アイテム数(`SELECT COUNT(*) FROM cf_item WHERE cf_document_id = doc.id`)。`display_order ASC NULLS LAST, title ASC, identifier ASC` でソート。各ドキュメントのタイトルは `/{tenant-uuid}/cftree/doc/{doc-uuid}` へのリンク。ドキュメントが0件の場合は「フレームワークはありません」を表示 | +| GET /{tenant-uuid}/ | フレームワーク一覧: CFDocumentのtitle, lastChangeDateTime, アイテム数(`SELECT COUNT(*) FROM cf_item WHERE cf_document_id = doc.id`), ルーブリック数(そのドキュメントの CFRubrics の `COUNT`)。`display_order ASC NULLS LAST, title ASC, identifier ASC` でソート。各ドキュメントのタイトルは `/{tenant-uuid}/cftree/doc/{doc-uuid}` へのリンク。ドキュメントが0件の場合は「フレームワークはありません」を表示 | | GET /{tenant-uuid}/cftree/doc/{doc-uuid} | ツリービュー。**遅延ツリー** — 初期は深さ0-1 をネイティブ `
` で SSR、深い階層は展開時に `/children/` で取得。`?item={uuid}`(後方互換)で当該アイテムまでの祖先パスと右ペイン詳細を SSR | | GET /{tenant-uuid}/cftree/doc/{doc-uuid}/children/{parent-uuid} | 親アイテムの子1階層の HTML フラグメント(遅延ツリーの展開用)。初期 SSR と同じ `tree_nodes.html` マークアップ | | GET /{tenant-uuid}/cftree/doc/{doc-uuid}/item/{item-uuid} | アイテムを **URL パス**で選択したツリービュー。ツリー内ナビが push する正規・共有可能・リロード安全な形式(`hx-push-url`)。直接開く/リロード/共有でツリー(当該アイテムまでの祖先パスを展開)+フル詳細を SSR 再構築 | @@ -388,13 +389,13 @@ OpenSALT のツリービューを参考にした 2 ペイン構成。見た目 - `GET /{tenant}/uri/{uuid}`: リソース種別による。CFItem → 「{fullStatement の先頭50文字} - COMPEITO」。CFDocument → 「{title} - COMPEITO」。lookup/CFAssociation → 「{title or identifier} - COMPEITO」 - エラーページ: 「{ステータスコード} - COMPEITO」 -**HTML `` 属性:** `base.html` で `lang="ja"` を固定値として設定する(管理UIの言語が日本語であるため)。`/uri/` ページでリソースに `language` フィールドがある場合も `` は変更しない(コンテンツの言語は `lang` 属性ではなくリソースの `language` フィールドで表現される)。 +**HTML `` 属性:** `base.html` はリクエスト言語(`{{ lang|default('en') }}`)から `lang` を設定する。言語は `Accept-Language` ヘッダーから決定(既定は `en`。`i18n.parse_accept_language` 参照)。UI は en/ja のバイリンガル。これは*UI の言語*であり、リソースのコンテンツ言語は別途リソース自身の `language` フィールドで表現され、`` には影響しない。 **ナビゲーション:** 全ページ共通でパンくずリンクをヘッダーに表示する: - `GET /`: パンくずなし(トップページ自体) - `GET /{tenant}/`: 「[テナント一覧](/)」 - `GET /{tenant}/cftree/doc/{doc}`: 「[テナント一覧](/) > [テナント名](/{tenant}/) > ドキュメントタイトル」(最後の要素は現在のページなのでリンクなし) -- `GET /{tenant}/uri/{uuid}`: リソース種別による。CFItem・CFAssociation → 「[テナント一覧](/) > [テナント名](/{tenant}/)」(所属ドキュメントはページ内の CFDocumentURI に表示)。CFDocument → 「[テナント一覧](/) > [テナント名](/{tenant}/)」。lookup リソース → 「[テナント一覧](/) > [テナント名](/{tenant}/)」 +- `GET /{tenant}/uri/{uuid}`: リソース種別による。CFItem・CFAssociation・CFRubric は所属ドキュメントが解決されるので3段目にそのリンクを出す → 「[テナント一覧](/) > [テナント名](/{tenant}/) > [ドキュメントタイトル](/{tenant}/cftree/doc/{doc})」。CFDocument・lookup リソース(所属ドキュメントなし)は2段「[テナント一覧](/) > [テナント名](/{tenant}/)」。ページ本文にも CFDocumentURI でドキュメントを表示。 ``` ┌─────────────────────────────────────────────────────┐ @@ -425,6 +426,7 @@ OpenSALT のツリービューを参考にした 2 ペイン構成。見た目 - **同一インスタンスの別テナント(public)** → 関連は**宣言した側のテナントが所有**するが、`*NodeURI` が同一 compeito インスタンス上の*公開*別テナントの CFItem(`{base_url}/{tenant}/uri/{item}` 形式の permalink。`uri_service.parse_internal_tenant_id` で判定)を指す場合は、相手の title を解決し**相手テナントのツリー**へリンク(`/{別テナント}/cftree/doc/{別doc}/item/{id}`)+「他機関」バッジ(`related_other_institution`)。`web._resolve_cross_tenant` → `tenant_service.get_tenant`(可視性チェック)+ `cf_item_repository.map_identifiers_to_items` でサーバー側解決(`related_other_tenant` / `assoc_node_*` が `tenant_segment` を持つ)。 - **同一インスタンスの別テナント(private)** → **完全非表示**: private(または存在しない)テナントを指す端点は丸ごと除外(title・URI・リンク・バッジ いずれも出さない)。存在自体を一切出さない。 - **外部 / 解決不能** → 保存済み URI を別タブ(`target=_blank`)+「外部」バッジ。**http(s) のみ**リンク化し、それ以外(`javascript:` / `data:`)はプレーンテキスト。public 項目に解決できなかった `{base_url}` 内部 URI はここでリンク化せず、上記の private/非表示扱いになる。 + - **参照元(他機関)**: 上記 public クロステナントの逆方向。CFItem のペインに、**他テナントが所有する** CFAssociation のうち**この項目を destination として指している**ものを一覧する(=このインスタンス上で誰がこの項目を採用/参照しているか)。`web._incoming_refs` でサーバー側解決: この項目の permalink(`{base_url}/{tenant}/uri/{identifier}`)を組み立て、`cf_association_repository.list_incoming_by_destination_uri` でテナント横断に逆引き(`isChildOf` は除外)、現テナント発のものを除外し、外向きと同様に **public 限定**=**private** テナント所有の関連は解決前に除外する(private な採用元は順方向でも逆方向でも一切出さない)。各エントリは origin 項目の title を「他機関 ↩」バッジ付きで相手テナントのツリーへリンク(`incoming_refs` / `related_other_institution_in`、`incoming_refs_label`)。 - ペイン内の「対象アイテムへ」/親リンクは**経路非依存のラベル**(`view_linked_item`「対象アイテムを表示」、`go_to_top_page`)で、どの経路で来ても自然に読める。 - **クロス文書の階層(上位/下位 別FW)**: CFItem の `isChildOf` の親/子が**別フレームワーク**(大規模フレームワークを複数ドキュメントに分割した場合)に在るとき、ペインに「上位(別フレームワーク)/下位(別フレームワーク)」節を出す。ツリーは per-doc のままで、境界の親/子だけここに出す(同一ドキュメントの階層はツリーにあるので省く)。各エントリは別FWへのツリー切替リンク(または外部リンクアウト)。テナント横断の `isChildOf` から解決(`cf_association_repository.list_ischildof_parents` / `_children` + `cf_item_repository.map_identifiers_to_items`、`_cross_doc_hierarchy`)。isChildOf の向き: origin=子・destination=親。クロステナントの出し分けも同様: **public 別テナント**の親/子は相手テナントのツリーへ「他機関」バッジ付きでリンク、**private** 別テナントのものは除外(出さない)。 - **右ペイン = フル詳細。** ペインは**単独 `/uri/{uuid}` ページと同じフル詳細カード**(共通パーシャル `fragments/resource_detail.html`)を描画する。permalink・API URL・実効ライセンス・関連(grouping 別)など全フィールドが、ツリーを離れずに見える。右ペイン用フラグメントと deep-link `?item=` の SSR はどちらもこのフルカードを返す。(従来の軽量サマリ+「詳細」リンクへの往復は廃止)