From 599cc5778ae268466333b81878914425f58571f8 Mon Sep 17 00:00:00 2001 From: Kentaro Saida Date: Sat, 20 Jun 2026 14:15:24 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(import):=20caseVersion=20=E3=81=AE=20i?= =?UTF-8?q?mport=20=E6=A4=9C=E8=A8=BC=EF=BC=88=E6=83=B3=E5=AE=9A=E5=A4=96?= =?UTF-8?q?=E5=80=A4=E3=82=92=E8=AD=A6=E5=91=8A=EF=BC=89=20(C8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CFPackage import 時、caseVersion が想定外(1.0 / 1.1 以外)の場合に警告を 追加する。値そのものは保持する(emit を固定すると OpenCASE round-trip の 忠実度が崩れるため、検証=警告にとどめる)。1.0 は v1.0 取り込みのため許容。 - _validate_case_version ヘルパーを追加、_create_document / _update_document で使用 - tests/unit/test_case_import.py に検証ケースを追加 - backlog C8 を「対応済み」へ 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 +- src/services/case_import_service.py | 26 +++++++++++++++++++++-- tests/unit/test_case_import.py | 25 ++++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/docs/dev/case-v1p1-conformance-backlog.md b/docs/dev/case-v1p1-conformance-backlog.md index a806f4e..7cbee20 100644 --- a/docs/dev/case-v1p1-conformance-backlog.md +++ b/docs/dev/case-v1p1-conformance-backlog.md @@ -16,6 +16,7 @@ compeito の現在のゴールは **OpenCASE / OpenSALT との実用的な相互 - Service Discovery `GET /ims/case/v1p1/discovery/imscasev1p1_openapi3_v1p0.json`(実装済・テストあり) - エラー封筒 `imsx_StatusInfo`(codeMajor / severity / codeMinor.codeMinorField[].{Name,Value})は適合 - `GET /CFItemAssociations/{id}` の既定を全件返却に(公式契約にページネーション定義なし。既定 limit=100 のサイレント切り詰めを廃止、`limit`/`offset` は明示指定時のみの拡張に。2026-06 適合性監査 N1、PR #220) +- **`caseVersion` の import 検証**(旧 C8)。想定外の値(`1.0` / `1.1` 以外)は警告を出す。値そのものは保持する(round-trip 忠実度を保つため emit 固定はしない)。 ## certification 着手項目(未対応 / 意図的差異) @@ -28,7 +29,6 @@ compeito の現在のゴールは **OpenCASE / OpenSALT との実用的な相互 | C5 | **`Link` ページネーションヘッダ** | 未実装(next/prev/first/last)。`X-Total-Count` は実装済み | P2 | `GET /CFDocuments` に RFC 8288 形式の `Link` を追加。既存クエリ(sort/filter/fields)を保持してリンク生成 | | C6 | **filter の網羅性** | scalar + `subject`(JSONB) 対応。ネストのドット記法(`licenseURI.identifier` 等)未対応。ordering は大小区別のまま(等価は大小無視に対応済) | P2 | dot-notation のリンクフィールド filter、必要なら collation 指定の case-insensitive ordering | | C7 | **不明な `sort` / `fields` → 400** | binding 散文は「不明 sort は既定順」「不明 field は全件返す(空 field のみ invalid_selection_field)」。compeito は 400 で明示エラー(typo 可視で親切) | P2 | strict/compat モードでのみ binding 散文どおりの寛容挙動に切替(既定は現状維持を推奨) | -| C8 | **`caseVersion` を "1.1" に強制しない** | 保存値をそのまま emit | P3 | import 時に "1.1" 検証、または emit 時に固定 | | C9 | **UUID 不正 → 400 / `limit`=0 許容 / `limit`・`offset` の上限 cap** | 実用優先の挙動(OpenAPI は invalid を unknownobject 扱い、`minimum:1` 等) | P3 | strict モードでのみ OpenAPI どおりに(既定は現状維持) | | C10 | **拡張 list エンドポイント** | `CFItemTypes` 等の list は compeito 拡張(公式 list は `CFDocuments` のみ)。`sort/filter/fields` も `CFDocuments` のみ対応 | — | 仕様超過なので certification 上は無害。必要なら他 list にも query を展開 | | C11 | **エラー封筒の `imsx_codeMinorFieldName`** | 常に既定の `"sourcedId"`。invalid_sort_field / invalid_selection_field 系では `sort` / `fields` / `limit` 等の実フィールド名が意味的に正しい | P3 | `imsx_error_response` に fieldName 引数を追加し、各呼び出し箇所で該当フィールド名を渡す | diff --git a/src/services/case_import_service.py b/src/services/case_import_service.py index fb13790..a7516f0 100644 --- a/src/services/case_import_service.py +++ b/src/services/case_import_service.py @@ -254,6 +254,28 @@ def _validate_language( return val +# Accepted caseVersion values. The CASE v1.1 OpenAPI enum is just "1.1"; we also +# accept "1.0" because compeito ingests v1.0 sources (normalized to v1.1 output). +_VALID_CASE_VERSIONS = {"1.0", "1.1"} + + +def _validate_case_version( + val: str | None, + context: str, + warnings: list[str], +) -> str | None: + """Warn on an unexpected caseVersion; the stored value is left unchanged. + + A warning (not a rewrite) keeps round-trip fidelity intact while still + surfacing malformed values such as "2.0" / "v1.1" / typos. + """ + if val is not None and val not in _VALID_CASE_VERSIONS: + warnings.append( + f"{context}: unexpected caseVersion '{val}' (expected one of {sorted(_VALID_CASE_VERSIONS)}); kept as-is" + ) + return val + + # --------------------------------------------------------------------------- # HTTP fetch # --------------------------------------------------------------------------- @@ -901,7 +923,7 @@ def _create_document( publisher=data.get("publisher"), description=data.get("description"), framework_type=data.get("frameworkType"), - case_version=data.get("caseVersion"), + case_version=_validate_case_version(data.get("caseVersion"), f"CFDocument '{ident}'", warnings), language=lang, version=data.get("version"), adoption_status=data.get("adoptionStatus"), @@ -954,7 +976,7 @@ def _update_document( if data.get("frameworkType") is not None: doc.framework_type = data["frameworkType"] if data.get("caseVersion") is not None: - doc.case_version = data["caseVersion"] + doc.case_version = _validate_case_version(data["caseVersion"], f"CFDocument '{doc.identifier}'", warnings) if data.get("notes") is not None: doc.notes = data["notes"] if data.get("extensions") is not None: diff --git a/tests/unit/test_case_import.py b/tests/unit/test_case_import.py index 99a235e..a777e44 100644 --- a/tests/unit/test_case_import.py +++ b/tests/unit/test_case_import.py @@ -28,6 +28,7 @@ _normalize_v1p0_package, _parse_sequence_number, _validate_association, + _validate_case_version, _validate_cf_package, fetch_cf_package, import_case_from_dict, @@ -378,6 +379,30 @@ def test_empty_title(self): _validate_cf_package(pkg) +class TestCaseVersionValidation: + def test_v11_no_warning(self): + warnings: list[str] = [] + assert _validate_case_version("1.1", "CFDocument 'x'", warnings) == "1.1" + assert warnings == [] + + def test_v10_no_warning(self): + warnings: list[str] = [] + assert _validate_case_version("1.0", "CFDocument 'x'", warnings) == "1.0" + assert warnings == [] + + def test_none_no_warning(self): + warnings: list[str] = [] + assert _validate_case_version(None, "CFDocument 'x'", warnings) is None + assert warnings == [] + + def test_unexpected_value_warns_but_keeps_value(self): + warnings: list[str] = [] + # Value is preserved (round-trip fidelity); only a warning is emitted. + assert _validate_case_version("2.0", "CFDocument 'x'", warnings) == "2.0" + assert len(warnings) == 1 + assert "caseVersion" in warnings[0] + + class TestAssociationValidation: def test_valid_association(self): assoc = _make_association() From edc3fe3d803046a762c84769ecab596d3765a9fe Mon Sep 17 00:00:00 2001 From: Kentaro Saida Date: Sat, 20 Jun 2026 14:23:26 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20caseVersion=20=E3=81=AE=20import=20?= =?UTF-8?q?=E6=8C=99=E5=8B=95=E3=82=92=E5=AE=9F=E8=A3=85=E3=81=AB=E8=BF=BD?= =?UTF-8?q?=E5=BE=93=EF=BC=88Codex=20=E6=8C=87=E6=91=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #8 で caseVersion の受理が「1.1 のみ」から「1.0/1.1 を受理し、 その他は警告のうえ値を保持」に変わったのに、ドキュメントが古いままだった。 - import-logic.md: 外部CASEインポートの caseVersion 説明(英 line 295 / 日 866) を新挙動に更新 - db-schema.md: case_version カラムコメント(英/日)を「import は 1.0 も受理・ その他は警告のうえ保持」に明確化 - web-ui.md: フィールド表の caseVersion を同様に明確化 import-logic.md の v1.0「検出」ロジック(caseVersion=="1.0" で v1.0 判定)は C8 と無関係なので変更なし。 Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XXFogp18twGiPFBAZcqJw2 --- docs/spec/db-schema.md | 4 ++-- docs/spec/import-logic.md | 4 ++-- docs/spec/web-ui.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/spec/db-schema.md b/docs/spec/db-schema.md index 0c88506..4bbf751 100644 --- a/docs/spec/db-schema.md +++ b/docs/spec/db-schema.md @@ -52,7 +52,7 @@ creator: VARCHAR -- Required in CASE v1.1 but nullable here to a publisher: VARCHAR description: TEXT framework_type: VARCHAR -- v1.1 new. Standard value "CourseCodes" (free-form string per OpenAPI). -case_version: VARCHAR -- v1.1 new. OpenAPI enum: ["1.1"]. Only "1.1" is valid. +case_version: VARCHAR -- v1.1 new. OpenAPI enum: ["1.1"]. Import also accepts "1.0" (v1.0 sources); other values are kept with a warning. language: VARCHAR(10) version: VARCHAR adoption_status: VARCHAR @@ -301,7 +301,7 @@ creator: VARCHAR -- CASE v1.1 では required だが、CSV イ publisher: VARCHAR description: TEXT framework_type: VARCHAR -- v1.1 new. 標準値は "CourseCodes"(OpenAPI 上は自由文字列) -case_version: VARCHAR -- v1.1 new. OpenAPI では enum: ["1.1"]。値は "1.1" のみ有効 +case_version: VARCHAR -- v1.1 new. OpenAPI では enum: ["1.1"]。import は "1.0"(v1.0 ソース)も受理し、その他の値は警告のうえ保持 language: VARCHAR(10) version: VARCHAR adoption_status: VARCHAR diff --git a/docs/spec/import-logic.md b/docs/spec/import-logic.md index ae6f291..97f61c1 100644 --- a/docs/spec/import-logic.md +++ b/docs/spec/import-logic.md @@ -292,7 +292,7 @@ Mapping from the external CFPackage's CFDocument object to DB columns: - `publisher` → `publisher`. - `description` → `description`. - `frameworkType` → `framework_type` (v1.1 new). -- `caseVersion` → `case_version` (v1.1 new; only `"1.1"` is valid). +- `caseVersion` → `case_version` (v1.1 new). `"1.0"` and `"1.1"` are accepted without a warning (`"1.0"` because compeito ingests v1.0 sources); any other value emits a warning ("CFDocument '{id}': unexpected caseVersion '{val}' …") and the value is **kept as-is** (not rewritten, to preserve round-trip fidelity). - `language` → `language` (validate length ≤ 10; too long → NULL with a warning — same rule as CSV import). - `version` → `version`. - `adoptionStatus` → `adoption_status`. @@ -863,7 +863,7 @@ CSVインポートと同様に、既存ドキュメント更新時は Step 3 で - `publisher` → `publisher` - `description` → `description` - `frameworkType` → `framework_type`(v1.1 new) -- `caseVersion` → `case_version`(v1.1 new。値は `"1.1"` のみ有効) +- `caseVersion` → `case_version`(v1.1 new。`"1.0"` と `"1.1"` は警告なしで受理する(`"1.0"` は v1.0 ソース取り込みのため)。それ以外の値は警告を出力し(「CFDocument '{id}': unexpected caseVersion '{val}' …」)、値はそのまま保持する(round-trip 忠実度を保つため書き換えない)。) - `language` → `language`(10文字以下であることを検証する。超過の場合は NULL として保存し警告出力。CSV インポートと同一ルール) - `version` → `version` - `adoptionStatus` → `adoption_status` diff --git a/docs/spec/web-ui.md b/docs/spec/web-ui.md index 3d9de01..8f9b9b4 100644 --- a/docs/spec/web-ui.md +++ b/docs/spec/web-ui.md @@ -534,7 +534,7 @@ OpenSALT の `/uri/{uuid}` ページを参考にしつつ、デザインは Tail | licenseURI | 任意 | ネスト表示(title, identifier, uri)。CFItem と同一形式 | | officialSourceURL | 任意 | URL(リンク) | | frameworkType | 任意 | テキスト(v1.1 new。外部インポート由来で設定される場合がある) | -| caseVersion | 任意 | テキスト(v1.1 new。値は "1.1" のみ) | +| caseVersion | 任意 | テキスト(v1.1 new。OpenAPI enum は "1.1"。import は "1.0" も受理し、その他の値は警告のうえ保持) | | subject | 任意 | 配列をカンマ区切りで表示 | | subjectURI | 任意 | 各要素のネスト表示(title, identifier, uri)。配列 | | CFPackageURI | 必須 | ネスト表示(title, identifier, uri) |