From 07b9c1b4f0f9e769428d303fce2ca67e9a0ab5e2 Mon Sep 17 00:00:00 2001 From: rimtty Date: Mon, 7 Sep 2026 22:39:09 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20describe=20the=20third=20ship-to=20dest?= =?UTF-8?q?ination=20=E3=83=87=E3=83=B3=E3=82=BD=E3=83=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRODUCT_SPEC, CLAUDE.md and the iOS/Android docs now cover three destinations: the JAMA self-describing kanban QR parsed generically with denso probed first, the 6-4 product tag, the QR-only duplicate key and per-part box count, the destination-aware part-number formatting, the Android length-hint rules, the history/PDF kanban fields, and the 47-case shared fixture. Refs #112 --- CLAUDE.md | 23 +++++++++---------- docs/PRODUCT_SPEC.md | 20 +++++++++++------ docs/android/IMPLEMENTATION_PLAN.md | 34 ++++++++++++++++------------- docs/android/STATUS.md | 16 ++++++++++++-- docs/android/TEST_PARITY.md | 32 ++++++++++++++++++++++++--- docs/ios/IMPLEMENTATION_GUIDE.md | 18 ++++++++++----- 6 files changed, 99 insertions(+), 44 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6bc60d4..a4e111f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,9 +16,10 @@ The cross-platform behavior contract lives in [docs/PRODUCT_SPEC.md](docs/PRODUC - **Android is for personal, local use only.** APKs are built and side-loaded by the owner; there is no store submission. Do not add store-submission work. The remaining physical-device / manual gates (Samsung, TalkBack, BLE fault cases, etc.) are waived and will not be verified further; `docs/android/STATUS.md` lists them under 打ち切った確認項目 (see issue #57). Scanner SDK libraries are bundled in the normal `release` build (#56); there is no separate `scannerPoc` build type anymore. - **iOS is complete.** The "scanner stops reading after continuous use" audit (#58, closed 2026-09-06) landed as #62–#66: setting writes retry and recover instead of parking the session on the camera, `.inactive` no longer rewrites symbologies, the FF04 helper link has a deadline, the command timeout is 10 s with SDK link liveness checks, and the diagnostics log (300 events) can be shared from Settings. Verified on HPRT-4F5F; BCST-47 verification continues through TestFlight 1.0 (4). - **Scanner tuning is applied on connect by both apps** (#59, closed): illumination `lighting_lamp_control` = 2 with a Settings toggle, plus a fixed profile (multi-code `*_read_more_code`/`*_read_multi` = 0, inverse `read_inverse_color`/`*_read_phase` = 0, red-light time `auto_close_mode` = 20 ≈ 4 s) written only when the inventory differs and confirmed by readback. Values persist on the scanner and are not restored on disconnect. The inventory-name ↔ generic-flag table is in `docs/ios/IMPLEMENTATION_GUIDE.md`. Per-step symbology on iOS (#39) is deferred. -- **Customer-facing data spec**: `docs/qr-barcode-spec-analysis.html` / `.pdf` are deliberately git-ignored (local only). They now cover both ship-to destinations: the 澤井製作所 QR field table with the 12 real label pairs, and §6 for モルテン (§6.6 holds the 2026-09-06 decisions). Keep them in sync when matching or scan-acceptance rules change, and regenerate the PDF from the HTML with headless Chrome. -- The 2026-09-06 spec audit follow-ups landed: camera Code 128 gets the same `4-2-4@code` format validation as BLE on both platforms (#78), and `shared/test-fixtures/matching-cases.json` pins the real label pairs plus same-series mix-ups as mismatches (#79). The fixture is now `schemaVersion` 2 with 35 cases, each carrying the `destination` its QR belongs to: the 12 real 66-char 澤井製作所 decodes, two boxes of one part (`box-a` / `box-b`) that ground the duplicate rule, the real 61-char モルテン records, and two destination-less cases that must never match. The label photos live in git-ignored `tmp/sawai-labels/` (customer data, never commit); decode them with a Vision `VNDetectBarcodesRequest` script when the label set changes. -- **Ship-to destinations (仕向地) landed 2026-09-07** (#84; PRs #93–#100). A slip QR is either 澤井製作所 (`sawai`, 66 chars) or モルテン (`molten`, 61 chars whose trailing spaces are data). The first accepted QR locks the session's destination, and a QR of the other destination is rejected until the operator ends the session — no mixed sessions. The duplicate key is the QR alone for sawai and QR + Code 128 for molten; molten boxes are counted per 納品番号 with a cumulative 収容数, and neither app judges completion. Android persists the lock in Room v3 (`sessions.destination`, `scan_checkpoints.destination`, `MIGRATION_2_3`). Existing user-facing strings were left unchanged. +- **Customer-facing data spec**: `docs/qr-barcode-spec-analysis.html` / `.pdf` are deliberately git-ignored (local only). They now cover all three ship-to destinations: the 澤井製作所 QR field table with the 12 real label pairs, §6 for モルテン (§6.6 holds the 2026-09-06 decisions), and §7 for デンソー (§7.5 holds the 2026-09-07 decisions). Keep them in sync when matching or scan-acceptance rules change, and regenerate the PDF from the HTML with headless Chrome. +- The 2026-09-06 spec audit follow-ups landed: camera Code 128 gets the same `4-2-4@code` format validation as BLE on both platforms (#78), and `shared/test-fixtures/matching-cases.json` pins the real label pairs plus same-series mix-ups as mismatches (#79). The fixture is now `schemaVersion` 2 with 47 cases, each carrying the `destination` its QR belongs to: the 12 real 66-char 澤井製作所 decodes, two boxes of one part (`box-a` / `box-b`) that ground the duplicate rule, the real 61-char モルテン records, the real 221-char デンソー kanbans, and two destination-less cases that must never match. The label photos live in git-ignored `tmp/sawai-labels/` (customer data, never commit); decode them with a Vision `VNDetectBarcodesRequest` script when the label set changes. +- **Ship-to destinations (仕向地) landed 2026-09-07** (#84; PRs #93–#100). A slip QR is 澤井製作所 (`sawai`, 66 chars), モルテン (`molten`, 61 chars whose trailing spaces are data) or, since #106, デンソー. The first accepted QR locks the session's destination, and a QR of another destination is rejected until the operator ends the session — no mixed sessions. The duplicate key is the QR alone for sawai and QR + Code 128 for molten; molten boxes are counted per 納品番号 with a cumulative 収容数, and neither app judges completion. Android persists the lock in Room v3 (`sessions.destination`, `scan_checkpoints.destination`, `MIGRATION_2_3`). Existing user-facing strings were left unchanged. +- **仕向地 デンソー landed 2026-09-07** (#106; PRs #113–#116, iOS history/PDF #117). Its kanban QR is a JAMA self-describing record — `JAMA` + version digit + 4-digit header length + 10-char preamble + N × (3-digit item id + 2-digit length) + fixed-width data whose lengths must sum exactly — so it is parsed generically, not by length (the real kanban is 221 chars, header 119, 21 items, 97 data chars). Items 104 (10-digit part number), 112 (収容数) and 152 (かんばん連番, unique per box) are required. `Destination.detect` probes denso **first**, because the lenient sawai parse would otherwise accept `JAMA5011…` as a card number. The product tag is `6-4@code` (`860150-7722@1DZ50O`), and part numbers print `6-4` through the destination-aware `format(partNumber:destination:)` / `formatPartNumber(partNumber, destination)` — the 1-arg / defaulted form is gone on both platforms so the compiler enumerates every call site. Duplicates key on the QR alone and boxes are counted per part number, exactly like sawai; there is no planned-box-count display and no completion judgement. No schema change: Room stays v3 with `denso` stored as the `destination` string, and iOS history JSON keeps decoding it leniently. - Work items that should be tracked go into GitHub Issues (`gh issue create`). ## Git conventions @@ -91,27 +92,27 @@ Flow: `CodeMatchApp` → `RootTabView` (owns the single `HistoryStore`, `Bluetoo - **`CameraScanner`** owns `AVCaptureSession` on a private serial `sessionQueue`, with metadata delivered on a second queue and hopped back to `@MainActor` before touching the delegate. Only `AVCaptureMetadataOutput` with `[.qr, .code128]` — deliberately no `AVCapturePhotoOutput`; do not add one. - **`BluetoothScannerService`** wraps the Inateck iOS SDK (`#if INATECK_SDK`, simulator mock otherwise): discovery, connect/auto-reconnect, and symbology restriction. During a session it writes all symbology areas (flag 2001–2028) to 0 except `qrcode_on` / `code128_on`, using the `area`/`name` the device reported (never hardcoded areas), snapshots the original values, and restores them on session end/disconnect. All `setSettingInfo` traffic (session symbologies, illumination, tuning) goes through one gate (`settingCommandInFlight`, generation counters, 10 s timeout that closes the link and reconnects); failed writes retry twice and then recover from a fresh `getSettingInfo` every 5 s (max 3). After the session symbologies are ready the service applies illumination OFF, then the tuning profile, each confirmed by readback. Diagnostics keep 300 trace lines (no payloads) and `diagnosticLogText()` feeds the Settings share sheet. - **`HistoryStore`** (`@MainActor`) holds `[MatchSession]` newest-first; at most one session has `endedAt == nil` (`activeSession`). Persists to `Application Support/CodeMatch/match-history.json` with `.completeFileProtection`, excluded from backup. `init(storageURL:)` exists so tests can point at a temp directory. -- **`CodeMatcher`** is the whole comparison rule, and it is destination-aware. `Destination.detect(qrPayload:)` strips only CR/LF/NUL and decides between `sawai` (66-char `KanbanQRRecord`, part number at chars 11–20) and `molten` (61-char `MoltenQRRecord`, part number at chars 7–16; a payload of 57–61 chars is padded back to 61 because its trailing spaces are data). Sawai is retried on the trimmed payload only after molten is ruled out. The Code 128 is `PART-NO@code`; both sides are normalized and compared exactly — the old substring-containment fallback is gone, so a QR that parses as neither record can never match. `format(partNumber:)` prints 10 chars as 4-2-4 and 9 chars as 4-2-3. `BoxIdentity.make(qrPayload:barcodePayload:)` builds the per-box key: the QR text for sawai, canonical QR + `|` + Code 128 for molten. Tests pin this with real label payloads of both destinations through the shared fixtures. +- **`CodeMatcher`** is the whole comparison rule, and it is destination-aware. `Destination.detect(qrPayload:)` strips only CR/LF/NUL and decides between `denso` (variable-length JAMA `DensoKanbanRecord`, part number from item 104), `sawai` (66-char `KanbanQRRecord`, part number at chars 11–20) and `molten` (61-char `MoltenQRRecord`, part number at chars 7–16; a payload of 57–61 chars is padded back to 61 because its trailing spaces are data). Denso is probed first — the lenient `KanbanQRRecord.parse` would read `JAMA5011…` as a card number — and sawai is retried on the trimmed payload only after molten is ruled out. The Code 128 is `PART-NO@code`; both sides are normalized and compared exactly — the old substring-containment fallback is gone, so a QR that parses as no record can never match. `format(partNumber:destination:)` prints denso's 10 chars as 6-4, and otherwise 10 chars as 4-2-4 and 9 chars as 4-2-3; there is no 1-arg overload, so every call site must pass the destination it holds. `BoxIdentity.make(qrPayload:barcodePayload:)` builds the per-box key: the QR text for sawai and denso (denso's item 152 かんばん連番 differs per box), canonical QR + `|` + Code 128 for molten. Tests pin this with real label payloads of all three destinations through the shared fixtures. Two misread defenses that are intentional and easy to break accidentally: - `scanLocked` suppresses reads for 250ms after accepting a QR value. -- Code 128 from the camera requires the *same* value seen twice within 1.5s (`barcodeCandidate`) before it is accepted; only then does the camera stop and comparison run. A BCST-47 trigger is accepted once. Camera and BLE Code 128 are both validated against the locked destination's business format (`TagBarcodeRecord.isValidScanPayload(_:destination:)` — `4-2-4@code` for sawai, `4-2-3` or `4-2-4` for molten) before the candidate logic runs; an unrelated Code 128 is rejected with a message and a 2 s repeat suppression, exactly like an unrelated QR (#78). +- Code 128 from the camera requires the *same* value seen twice within 1.5s (`barcodeCandidate`) before it is accepted; only then does the camera stop and comparison run. A BCST-47 trigger is accepted once. Camera and BLE Code 128 are both validated against the locked destination's business format (`TagBarcodeRecord.isValidScanPayload(_:destination:)` — `4-2-4@code` for sawai, `4-2-3` or `4-2-4` for molten, `6-4` for denso; before the lock any of the three) before the candidate logic runs; an unrelated Code 128 is rejected with a message and a 2 s repeat suppression, exactly like an unrelated QR (#78). Audio/haptics: `FeedbackPlayer` synthesizes PCM tones via `AVAudioEngine` (ambient category, `.mixWithOthers`). It is a single shared instance (`FeedbackPlayer.shared`) — engine setup is expensive and per-view instances caused a re-render loop, so keep it that way. Individual code acceptance is haptic-only plus a short chirp; the success chime and the 4× failure alert fire only at the final verdict. Success is delayed ~0.28s after a barcode accept so the chirp and chime don't collide. -Only matches are recorded (`historyStore.recordMatch` on `.match`); mismatches never touch history or the header count. Duplicate detection keys on the box, never on the part number: `activeSessionContainsMatchedBox(qrPayload:barcodePayload:)` compares the QR payload alone for sawai and the canonical QR + Code 128 pair for molten. Several boxes of one part number are recorded as 1箱目, 2箱目, … while re-scanning the same box yields `.duplicate`, which is shown but not recorded. Molten counts per 納品番号 instead of per part number: `activeSessionDeliverySummary(deliveryNumber:)` returns the `DeliveryBoxSummary` (box count plus cumulative 収容数) the result message reports. +Only matches are recorded (`historyStore.recordMatch` on `.match`); mismatches never touch history or the header count. Duplicate detection keys on the box, never on the part number: `activeSessionContainsMatchedBox(qrPayload:barcodePayload:)` compares the QR payload alone for sawai and denso, and the canonical QR + Code 128 pair for molten. Several boxes of one part number are recorded as 1箱目, 2箱目, … while re-scanning the same box yields `.duplicate`, which is shown but not recorded; denso goes through this sawai path unchanged. Molten counts per 納品番号 instead of per part number: `activeSessionDeliverySummary(deliveryNumber:)` returns the `DeliveryBoxSummary` (box count plus cumulative 収容数) the result message reports. -The session's destination is locked by the first accepted QR (`ScannerViewModel.destination`, mirrored onto the session by `setActiveSessionDestinationIfNeeded`). `reset()`, `rereadQR()`, and a mismatch deliberately leave it alone, and a new view model restores it from `activeSession.resolvedDestination`; a QR of the other destination is rejected on both camera and BLE with the same message. Android mirrors all of this with `ScanSessionState.destination` / `recordedBoxes` (`matchedBoxIdentities`, `moltenSummary`), restored from the checkpoint. +The session's destination is locked by the first accepted QR (`ScannerViewModel.destination`, mirrored onto the session by `setActiveSessionDestinationIfNeeded`). `reset()`, `rereadQR()`, and a mismatch deliberately leave it alone, and a new view model restores it from `activeSession.resolvedDestination`; a QR of another destination is rejected on both camera and BLE with the same message, which names the locked destination. Android mirrors all of this with `ScanSessionState.destination` / `recordedBoxes` (`matchedBoxIdentities`, `moltenSummary`), restored from the checkpoint. ### Android Single `MainActivity` + Compose, Hilt DI, `NavigationSuiteScaffold` with three destinations (scan / history / settings), unidirectional `UiState` + `StateFlow`. -- **`core/model`, `core/matching`**: framework-free domain. `Destination` (`sawai` / `molten`, with the same persisted ids as Swift) lives in `core/model`; `CodeMatcher` (`detectDestination`, `expectedQrLength`, `canonicalQrPayload`, `boxIdentity`, `stripTransportTerminators`), `KanbanQrRecord`, `MoltenQrRecord`, and `TagBarcodeRecord.isValidScanPayload(payload, destination)` mirror the Swift rules and are tested against `shared/test-fixtures/matching-cases.json` (loaded from the test classpath). -- **`feature/scan`**: `ScanReducer` (pure state machine), `ScanStabilizer` (camera Code 128 needs the same value twice; BLE reads are accepted once; a camera value outside the tag format skips the stabilizer and is rejected by `ScanReducer` on the first frame), `ScanSessionCoordinator`, `ScanCheckpointMapping`. `ScanSessionState` carries the locked `destination` and the `recordedBoxes` that feed the duplicate rule and the per-納品番号 molten summary; a QR of the other destination is refused with `InvalidScanReason.WRONG_DESTINATION`, and an unparseable QR is classified against the locked record length (66 or 61) or, before the lock, against the whole 57–66 range. `app/.../scan/ScanViewModel` persists a checkpoint (step, session, accepted values, input source, destination) through Room/DataStore so an OS process kill restores the exact step. +- **`core/model`, `core/matching`**: framework-free domain. `Destination` (`sawai` / `molten` / `denso`, with the same persisted ids as Swift) lives in `core/model`; `CodeMatcher` (`detectDestination`, `expectedQrLength`, `canonicalQrPayload`, `boxIdentity`, `stripTransportTerminators`, `formatPartNumber(partNumber, destination)`), `KanbanQrRecord`, `MoltenQrRecord`, `DensoKanbanQrRecord`, and `TagBarcodeRecord.isValidScanPayload(payload, destination)` mirror the Swift rules and are tested against `shared/test-fixtures/matching-cases.json` (loaded from the test classpath). `expectedQrLength` returns null for `DENSO` because a JAMA kanban declares its own length, and `formatPartNumber` has no default argument so the compiler forces every caller to supply a destination. +- **`feature/scan`**: `ScanReducer` (pure state machine), `ScanStabilizer` (camera Code 128 needs the same value twice; BLE reads are accepted once; a camera value outside the tag format skips the stabilizer and is rejected by `ScanReducer` on the first frame), `ScanSessionCoordinator`, `ScanCheckpointMapping`. `ScanSessionState` carries the locked `destination` and the `recordedBoxes` that feed the duplicate rule and the per-納品番号 molten summary (`moltenResultSummary` returns null unless the lock is `MOLTEN`); a QR of another destination is refused with `InvalidScanReason.WRONG_DESTINATION`. `ScanReducer.invalidPayloadReason` classifies an unparseable QR against the locked record length (66 or 61) or, before the lock, against the whole 57–66 range — but a denso-locked session reports `INVALID_PAYLOAD` with no length hint, and so does an unlocked `JAMA`-prefixed payload (never "incomplete"), because a JAMA kanban has no fixed length. `app/.../scan/ScanViewModel` persists a checkpoint (step, session, accepted values, input source, destination) through Room/DataStore so an OS process kill restores the exact step. - **`core/data`**: Room (`CodeMatchDatabase`, schema v3, exported schemas in `core/data/schemas`) for history, Preferences DataStore for settings and the scan checkpoint. v3 adds the nullable `destination` column to `sessions` and `scan_checkpoints` (`MIGRATION_2_3`); `SessionDao.lockDestination` only writes it when it is still null, so a session can never change destination. Both stores are excluded from cloud backup and D2D transfer (`res/xml/backup_rules.xml`, `data_extraction_rules.xml`). -- **`core/export`**: A4 multi-page PDF via `PdfDocument`; saved through `CreateDocument` and shared through a `FileProvider` limited to `cache/codematch-pdf/`. `HistoryDeliveryGroups.kt` supplies `resolvedDestination()` and `moltenDeliveryGroups()`, so a molten report prints one delivery block per 納品番号 with its box count and cumulative 収容数 while a sawai report stays exactly as before. +- **`core/export`**: A4 multi-page PDF via `PdfDocument`; saved through `CreateDocument` and shared through a `FileProvider` limited to `cache/codematch-pdf/`. `HistoryDeliveryGroups.kt` supplies `resolvedDestination()`, `moltenDeliveryGroups()` and `densoRecord()`, so a molten report prints one delivery block per 納品番号 with its box count and cumulative 収容数, a denso report prints the kanban block (部品番号・収容数・指示数 / 次区・指示・納入日・便 / 管理番号・アイテムNo・受入) plus each box's かんばん連番, and a sawai report stays exactly as before. `HistoryPdfContent.appendGroup` branches `MOLTEN` / `DENSO` / else, so a denso kanban is never drawn as a sawai カード番号. - **`scanner/api`**: `ExternalScanner` contract shared by camera/BLE/fake. **`scanner/camera`**: CameraX + bundled ML Kit, ROI limited to the on-screen guide (square for QR, wide for Code 128), only the format expected by the current step. **`scanner/ble`**: SDK-agnostic safety core — command queue, connection coordinator, per-step symbology restriction (QR step enables only flag 2022, Code 128 step only 2008, fresh readback required before Ready, full restore on session end/background), known-device store, reconnect budget. **`scanner/inateck`**: adapter over the official Inateck Android SDK 2.0.0 (`AndroidInateckSdkGateway`, native notification parser via JNA), illumination control (`lighting_lamp_control`, default 2 = always off on each connection, not restored on disconnect), and the connect-time tuning profile (`InateckTuningSettings`, applied after illumination settles, differences only, readback-confirmed). The BLE diagnostic log keeps 300 events; Settings can share it (ACTION_SEND text) or save it (SAF) via host-owned actions in `SettingsRoute`. - **DI per build type**: `app/src/debug` binds `FakeExternalScanner`; `app/src/release` binds `InateckExternalScanner` (`releaseImplementation(project(":scanner:inateck"))`). Release is minified (R8 strips the SDK's raw-payload logging via `app/scanner-rules.pro`), arm64-v8a only, and signed with the debug keystore unless `codematchRelease*` Gradle properties supply a local keystore. `app/src/release/AndroidManifest.xml` adds `BLUETOOTH_SCAN` (neverForLocation) / `BLUETOOTH_CONNECT` and removes the legacy permissions the SDK manifest brings in. `UnavailableExternalScanner` remains in `app/src/main` as a fallback type only. - **Strings**: `values/strings.xml` is Japanese (default), `values-en/` English; Android lint's `MissingTranslation` check (run by `lintDebug`) fails the build if a key is missing in either, and `HistoryUiTextTest.japaneseAndEnglishHistoryResourcesHaveTheSameKeys` pins the same parity for the history module in JVM tests. In-app language and Android 13+ per-app locale are kept in sync by `AppLanguageSynchronizer`. @@ -121,7 +122,7 @@ Single `MainActivity` + Compose, Hilt DI, `NavigationSuiteScaffold` with three d - iOS user-facing strings live in `ios/CodeMatch/Resources/Localizable.xcstrings` (source language **ja**, English translations) and are resolved through `AppLocalization.string(...)` / `AppLanguage`; Japanese is the fallback regardless of system locale. **Every key must keep an explicit `ja` entry with `state: translated`** — otherwise Xcode emits no `ja.lproj/Localizable.strings` and English-locale hosts (CI) silently resolve Japanese to English. When adding copy, add both `ja` and `en` entries. - Both apps default to Japanese, and UI/instrumentation tests match on the literal Japanese labels (e.g. `一致しました`, `終了する`), so changing copy breaks tests on both platforms. - Keep SwiftUI view `init`s cheap and side-effect-free: they rerun on every parent body evaluation. Anything heavy (stores, `AVCaptureSession`, audio engines) or side-effectful (UserDefaults resets) belongs inside a `@StateObject(wrappedValue:)` autoclosure (see `RootTabView.makeHistoryStore()`) or a shared instance. Violating this saturated the main thread and hung every UI test. -- iOS UI tests drive the app through launch arguments: `-resetHistory`, `-resetLanguage`, `-resetAutoAdvance`, `-resetBluetoothScanner` / `-demoBluetoothConnected`, and `-demoMatch` / `-demoMismatch`. Keep these sites in sync when adding a hook. The molten flow needed no new argument: it is driven by the simulator-only `demoBluetoothMoltenQRButton` / `demoBluetoothMoltenBarcodeButton` demo buttons next to the existing Bluetooth ones. +- iOS UI tests drive the app through launch arguments: `-resetHistory`, `-resetLanguage`, `-resetAutoAdvance`, `-resetBluetoothScanner` / `-demoBluetoothConnected`, and `-demoMatch` / `-demoMismatch`. Keep these sites in sync when adding a hook. The molten and denso flows needed no new argument: they are driven by the simulator-only `demoBluetoothMoltenQRButton` / `demoBluetoothMoltenBarcodeButton` and `demoBluetoothDensoQRButton` / `demoBluetoothDensoBarcodeButton` demo buttons next to the existing Bluetooth ones. Android's equivalents are the `ScanViewModel.SAMPLE_DENSO_QR_PAYLOAD` / `SAMPLE_DENSO_BARCODE_PAYLOAD` constants, which the instrumentation tests feed through the debug Fake scanner; the in-app demo action stays sawai. - Views are addressed in tests by accessibility identifiers (iOS `.accessibilityIdentifier`, Compose `testTag`) such as `startSessionButton`, `endSessionButton`, `sessionMatchCount`, `sessionDestination`, `resetButton`, `scannerTitle`, `historySessionRow`, `historySessionDestination`, `deliveryGroupRow`, `scan_session_destination`, `scan_result_molten_box_summary`; preserve them when refactoring. - Colors come only from `AppTheme` (iOS) / `CodeMatchTheme` (Android, contrast pinned at ≥ 4.5:1 by `CodeMatchThemeContrastTest`); iOS pins `.preferredColorScheme(.light)`. - Privacy: iOS declares no tracking in `Resources/PrivacyInfo.xcprivacy`; Android release must not request INTERNET, location, or legacy Bluetooth permissions (`docs/android/PRIVACY.md`). Any SDK addition must keep these boundaries. diff --git a/docs/PRODUCT_SPEC.md b/docs/PRODUCT_SPEC.md index 637cb54..dc0b230 100644 --- a/docs/PRODUCT_SPEC.md +++ b/docs/PRODUCT_SPEC.md @@ -13,20 +13,23 @@ 入力の順序は固定します。QR待機中にCode 128を受け取っても進まず、Code 128待機中にQRを受け取っても進みません。不一致は履歴へ保存しません。 -仕向地は作業セッション単位で固定します。最初に受理したQRの仕向地をそのセッションの仕向地として確定し、以後は別の仕向地のQRを受理せず、案内を表示して工程と一致件数を保ちます。固定はカメラ・Bluetoothで共通で、「次のコードを照合」「QRを読み取りなおす」でも不一致でも解除しません。仕向地を変えるにはセッションを終了します。 +仕向地は澤井製作所、モルテン、デンソーの3つです。仕向地は作業セッション単位で固定します。最初に受理したQRの仕向地をそのセッションの仕向地として確定し、以後は別の仕向地のQRを受理せず、案内を表示して工程と一致件数を保ちます。固定はカメラ・Bluetoothで共通で、「次のコードを照合」「QRを読み取りなおす」でも不一致でも解除しません。仕向地を変えるにはセッションを終了します。 ## 品番の抽出と比較 -QRのレコード様式は仕向地ごとに異なるため、先に仕向地を判定し、その様式の固定位置からだけ品番を取り出します。 +QRのレコード様式は仕向地ごとに異なるため、先に仕向地を判定し、その様式が定める場所からだけ品番を取り出します。 - 仕向地「澤井製作所」のQRは66桁固定長で、1〜10桁目がカード番号(英字4文字+数字6文字)、11〜20桁目が10桁の品目番号です。以降に枝番、納入数量、指示数、工場コード、受入部品庫、供給先が続きます。 - 仕向地「モルテン」のQRは61桁固定長で、7〜16桁目が左詰めの部品番号(9桁または10桁、余りは空白)です。ほかに受注者、納品番号、納入先、TYロケーション、供給先、収容数、納入指示日(JUMP)、時刻を固定位置に持ちます。末尾の空白も1レコードの一部なので、57〜61桁で届いた読取値は空白で61桁へ補完してから解析します。 -- Code 128は `品番@管理コード` 形式で、`@` より前を品番として使います。品番の表記は澤井製作所が `4-2-4`、モルテンは `4-2-3` または `4-2-4` です。 +- 仕向地「デンソー」のQRはJAMA自己記述形式のかんばんレコードで、桁数は固定しません。`JAMA` + 版1桁 + ヘッダ長4桁 + 前置き10桁 + 「項目番号3桁+桁数2桁」の並び + 固定長データ部で構成し、項目定義の桁数の合計がデータ部の長さと過不足なく一致することを確かめます。品番は項目104(ハイフンなし10桁)から取り出します。項目112(収容数)と項目152(かんばん連番)も必須です。実物のかんばんは221桁・ヘッダ長119・項目21個・データ部97桁ですが、桁数ではなくヘッダの宣言どおりに解析します。 +- Code 128は `品番@管理コード` 形式で、`@` より前を品番として使います。品番の表記は澤井製作所が `4-2-4`、モルテンは `4-2-3` または `4-2-4`、デンソーは `6-4` です。 - 品番は大文字化し、ハイフン・空白など英数字以外を除去して比較します。 - `@` 以降の管理コードは一致判定へ影響しません。 -- どちらの様式としても解析できないQRからは品番を取り出しません。空の値、品番を抽出できない値、異なる品番は不一致です。 +- いずれの様式としても解析できないQRからは品番を取り出しません。空の値、品番を抽出できない値、異なる品番は不一致です。 -QRの入力は、カメラ・Bluetoothとも伝送終端(CR/LF/NUL)だけを取り除いたうえで、仕向地ごとの桁数と必須フィールドを検証します。モルテンのレコードでは空白を取り除きません。Code 128の入力も、カメラ・Bluetoothとも `4-2-4@管理コード`(モルテンは `4-2-3@管理コード` も)の業務形式を検証し、逆順入力や業務外のコードを受理しません。カメラのCode 128は形式検証を通った値だけを同一値2回の確定候補にします。 +仕向地の判定はデンソーを最初に試します。澤井製作所のレコード解析は先頭10桁が英字4文字+数字6文字であればよいという寛容な条件のため、`JAMA5011...` で始まるかんばんをカード番号として受理してしまうからです。デンソーとして解析できないときにだけ、澤井製作所(66桁)、モルテン(57〜61桁)、空白を落とした澤井製作所の順で判定します。 + +QRの入力は、カメラ・Bluetoothとも伝送終端(CR/LF/NUL)だけを取り除いたうえで、仕向地ごとの桁数と必須フィールドを検証します。モルテンのレコードでは空白を取り除きません。デンソーのレコードも空白欄がデータなので取り除きません。Code 128の入力も、カメラ・Bluetoothとも `4-2-4@管理コード`(モルテンは `4-2-3@管理コード`、デンソーは `6-4@管理コード`)の業務形式を検証し、逆順入力や業務外のコードを受理しません。カメラのCode 128は形式検証を通った値だけを同一値2回の確定候補にします。 ## 重複判定 @@ -34,6 +37,7 @@ QRの入力は、カメラ・Bluetoothとも伝送終端(CR/LF/NUL)だけを - 澤井製作所: QR全文。カード番号が箱ごとに異なるため、QRだけで箱を識別できます。 - モルテン: 61桁へ正規化したQR全文とCode 128全文の組。QRは1品番1レコードで箱を区別しないため、現品票の管理コードまで含めて識別します。 +- デンソー: QR全文。かんばん連番(項目152)が箱ごとに異なるため、QRだけで箱を識別できます。 品番では判定しません。同じ品番の別の箱はそれぞれ記録し、同じ箱の再読取だけを重複とします。重複は結果として表示しますが、履歴と一致件数には加えません。 @@ -43,8 +47,9 @@ QRの入力は、カメラ・Bluetoothとも伝送終端(CR/LF/NUL)だけを - 澤井製作所: 品番ごとに数え、結果には「この品番は本セッションで n 箱目」を表示します。 - モルテン: 納品番号ごとに数え、結果には「納品番号 X は本セッションで n 箱目(累計 m 個)」を表示します。累計は同じ納品番号の各箱の収容数の合計です。 +- デンソー: 澤井製作所と同じく品番ごとに数え、結果には「この品番は本セッションで n 箱目」を表示します。かんばんの指示数と収容数から求まる予定箱数は表示しません。 -累計は進捗の目安であり、納品番号やセッションの完了はアプリで判定しません。 +累計は進捗の目安であり、納品番号やセッションの完了はアプリで判定しません。デンソーでも同様に、指示数に対する完了は判定しません。 ## 履歴とPDF @@ -53,6 +58,7 @@ QRの入力は、カメラ・Bluetoothとも伝送終端(CR/LF/NUL)だけを - 共通: 仕向地、セッション名、開始・終了時刻、一致件数、品番ごとの箱数、各箱の管理コードと両ペイロード全文。 - 澤井製作所: カード番号、品目番号、枝番、納入数量、指示数、工場コード、受入部品庫、供給先。 - モルテン: セッションの納品番号数と、納品番号ごとの箱数・累計収容数。各記録の受注者、部品番号、納品番号、納入先、TYロケーション、供給先、収容数、納入指示日(JUMP)、時刻。 +- デンソー: 履歴詳細には各記録の帳票区分、部品番号、包装、収容数、次区、指示、かんばん連番、管理番号、納入日、便、指示数、アイテムNo、受入を出します。PDFは品番ごとにかんばんの要約(部品番号・収容数・指示数、次区・指示・納入日・便、管理番号・アイテムNo・受入)を置き、その配下の各箱へかんばん連番と管理コードを並べます。納品番号ごとの集計はモルテンだけの出力であり、デンソーには出しません。 ## データとプライバシー @@ -60,4 +66,4 @@ QRの入力は、カメラ・Bluetoothとも伝送終端(CR/LF/NUL)だけを ## 共通テスト資源 -機械可読な入力と期待結果は [`shared/test-fixtures/matching-cases.json`](../shared/test-fixtures/matching-cases.json)、実機・エミュレーターで表示できる画像は [`shared/test-fixtures/images`](../shared/test-fixtures/images) に置きます。fixtureは`schemaVersion` 2で35ケースあり、各ケースはQRの仕向地を示す`destination`(`sawai` / `molten`)を持ちます。どちらの様式でもないQRを扱う`non-standard-qr-is-mismatch`と`empty-values`だけが`destination`を持ちません。画像は両仕向地のQRとCode 128を含み、モルテン分は `molten-` で始まる5点です。SwiftとKotlinの照合テストは、同じケースを満たすことを受け入れ条件とします。 +機械可読な入力と期待結果は [`shared/test-fixtures/matching-cases.json`](../shared/test-fixtures/matching-cases.json)、実機・エミュレーターで表示できる画像は [`shared/test-fixtures/images`](../shared/test-fixtures/images) に置きます。fixtureは`schemaVersion` 2で47ケースあり、各ケースはQRの仕向地を示す`destination`(`sawai` / `molten` / `denso`)を持ちます。いずれの様式でもないQRを扱う`non-standard-qr-is-mismatch`と`empty-values`だけが`destination`を持ちません。画像は3仕向地のQRとCode 128を含み、モルテン分は `molten-` で始まる5点、デンソー分は `denso-` で始まる3点です。SwiftとKotlinの照合テストは、同じケースを満たすことを受け入れ条件とします。 diff --git a/docs/android/IMPLEMENTATION_PLAN.md b/docs/android/IMPLEMENTATION_PLAN.md index 14577d1..6854408 100644 --- a/docs/android/IMPLEMENTATION_PLAN.md +++ b/docs/android/IMPLEMENTATION_PLAN.md @@ -55,11 +55,11 @@ BLEを除いた中間成果物は開発・評価用として成立する。Swift | 読み取り安定化 | QRは即時、カメラCode 128は1.5秒以内に同値2回で確定 | Scan stabilizer | Swiftテストと同じ入力系列で同じ確定結果 | | 読み直し | QRだけ破棄してQR工程へ戻る | `RereadQr`イベント | セッションと一致件数を維持し、選択入力元で再開 | | 入力順序 | 逆順を拒否し、値を照合に使わない | ドメイン状態機械で拒否 | 無効音・案内表示、工程・保存件数が変化しない | -| 仕向地 | 最初に受理したQRでセッションの仕向地を固定し、別の仕向地のQRを拒否 | Reducerの`destination`とRoomの列 | 混在セッションを作らず、プロセス再生成後も固定が残る | -| QR解析 | 仕向地判定と各固定位置フィールド(澤井製作所66文字 / モルテン61文字・末尾空白は有効) | 純Kotlin parser | Swiftの全fixtureとフィールド解析テストが一致 | -| Code 128解析 | `4-2-4@管理コード`(モルテンは`4-2-3@管理コード`も)、`@`以前を照合 | 純Kotlin parser | 正規化、管理コード除外、逆順拒否、仕向地ごとの末尾桁数が一致 | -| 照合 | 大文字化、英数字以外除去、仕向地ごとの固定位置だけを使う完全一致(包含fallbackなし) | 純Kotlin matcher | 共通JSON fixtureをSwift/Kotlinの両方で通す | -| 結果 | 一致/不一致、両品番表示、澤井製作所は同一品番の箱番号、モルテンは納品番号ごとの箱番号と累計収容数 | Result composable | 不一致は保存せず、一致だけ重複を含め保存 | +| 仕向地 | 澤井製作所 / モルテン / デンソーの3つ。最初に受理したQRでセッションの仕向地を固定し、別の仕向地のQRを拒否 | Reducerの`destination`とRoomの列 | 混在セッションを作らず、プロセス再生成後も固定が残る | +| QR解析 | 仕向地判定と各様式のフィールド(澤井製作所66文字 / モルテン61文字・末尾空白は有効 / デンソーはJAMA自己記述形式の可変長かんばん・項目104が部品番号) | 純Kotlin parser | Swiftの全fixtureとフィールド解析テストが一致 | +| Code 128解析 | `4-2-4@管理コード`(モルテンは`4-2-3@管理コード`も、デンソーは`6-4@管理コード`)、`@`以前を照合 | 純Kotlin parser | 正規化、管理コード除外、逆順拒否、仕向地ごとの区切りが一致 | +| 照合 | 大文字化、英数字以外除去、仕向地ごとの様式だけを使う完全一致(包含fallbackなし) | 純Kotlin matcher | 共通JSON fixtureをSwift/Kotlinの両方で通す | +| 結果 | 一致/不一致、両品番表示(品番表記は仕向地ごと。デンソーは`6-4`)、澤井製作所とデンソーは同一品番の箱番号、モルテンは納品番号ごとの箱番号と累計収容数 | Result composable | 不一致は保存せず、一致だけ重複を含め保存 | | 自動次工程 | 初期OFF、1/3/5秒、可視カウントダウン | coroutine + `StateFlow` | 一致時のみ開始。設定OFF、手動、終了、背景化でキャンセル | | 履歴一覧 | セッション降順、名前、日時、箱数、所要時間、削除 | Room + `LazyColumn` | 再起動後も一致。削除が永続化される | | セッション詳細 | 名前変更、開始/終了、箱数、品番数、品番グループ | list/detail navigation | 同一品番を集約し、最初/最後の時刻と箱数を表示 | @@ -212,15 +212,16 @@ Swift版のブランド色をMaterial 3の意味的な役割へ割り当てる 次をAndroid frameworkから分離した純Kotlinとして実装する。 - `CodeMatcher.normalize` -- `CodeMatcher.detectDestination` / `stripTransportTerminators` / `expectedQrLength` +- `CodeMatcher.detectDestination`(デンソーを最初に判定)/ `stripTransportTerminators` / `expectedQrLength`(デンソーは可変長なのでnull) - `partNumberFromQr` - `partNumberFromBarcode` - `KanbanQrRecord.parse/isValidScanPayload` - `MoltenQrRecord.parse/isValidScanPayload/canonicalPayload` -- `TagBarcodeRecord.parse/isValidScanPayload`(仕向地引数つき) +- `DensoKanbanQrRecord.parse/isValidScanPayload/canonicalPayload`(JAMAヘッダ長・項目定義・データ部の桁合計を検証し、項目104/112/152を必須とする) +- `TagBarcodeRecord.parse/isValidScanPayload`(仕向地引数つき。nilは3様式のOR) - `compare` -- `formatPartNumber` -- `CodeMatcher.boxIdentity`(澤井製作所はQR、モルテンはQR+Code 128) +- `formatPartNumber`(仕向地引数は必須。既定引数を持たせない) +- `CodeMatcher.boxIdentity`(澤井製作所とデンソーはQR、モルテンはQR+Code 128) - 品番ごとの箱グルーピングと、モルテンの納品番号ごとの箱グルーピング 最初のテストはSwift版の単体テストと同名または対応名にし、`shared/test-fixtures/matching-cases.json`を必ず読み込む。 @@ -252,13 +253,13 @@ MatchResult 入力元、カメラ稼働、BLE接続、カウントダウンは状態機械に従属させ、Composable内部に業務状態を分散させない。 -仕向地は状態機械が持ち、`EndSession`でだけ解除する。`RereadQr`、手動の次工程、不一致では固定を維持し、別仕向地のQRは`InvalidScanReason.WRONG_DESTINATION`として拒否する。解析できないQRの案内は、固定済みならその仕向地のレコード長(66または61)と比べ、未固定なら57〜66の範囲と比べて「途中で終わった」「余分な情報を含む」を出し分ける。記録済みの箱は`recordedBoxes`として状態機械に残し、重複判定(澤井製作所はQR、モルテンはQR+Code 128)と、モルテンの納品番号ごとの箱数・累計収容数の算出に使う。 +仕向地は状態機械が持ち、`EndSession`でだけ解除する。`RereadQr`、手動の次工程、不一致では固定を維持し、別仕向地のQRは`InvalidScanReason.WRONG_DESTINATION`として拒否する。解析できないQRの案内は、固定済みならその仕向地のレコード長(66または61)と比べ、未固定なら57〜66の範囲と比べて「途中で終わった」「余分な情報を含む」を出し分ける。デンソーは可変長なので長さ案内をせず、デンソーで固定済みのセッションと、未固定でも`JAMA`で始まる読取値は`InvalidScanReason.INVALID_PAYLOAD`とする。記録済みの箱は`recordedBoxes`として状態機械に残し、重複判定(澤井製作所とデンソーはQR、モルテンはQR+Code 128)と、モルテンの納品番号ごとの箱数・累計収容数の算出に使う。デンソーの箱数は澤井製作所と同じく品番ごとに数える。 ## 7. 保存、PDF、設定 ### 7.1 Room schema -現行schemaは v3(`core/data/schemas/{1,2,3}.json`)。v2で`scan_checkpoints`を追加し、v3で`sessions`と`scan_checkpoints`へ仕向地列を足した。 +現行schemaは v3(`core/data/schemas/{1,2,3}.json`)。v2で`scan_checkpoints`を追加し、v3で`sessions`と`scan_checkpoints`へ仕向地列を足した。仕向地は文字列で持つので、デンソーの追加ではschemaもcheckpointの契約versionも上げていない。 `sessions` @@ -308,6 +309,7 @@ MatchResult - Androidの`PdfDocument`でA4縦、複数ページを生成 - Swift版と同じセッション名、仕向地、開始/終了、箱数、品番数、QR解析値、管理コード、各payload、端末内生成の注記を出力 - モルテンのセッションではセッション見出しに納品番号数を加え、品番グループの中を納品番号ごとのブロック(納品番号、箱数、累計収容数、受注者、部品番号、納入先、TYロケーション、供給先、納入指示日(JUMP)、時刻)に分け、その配下に各箱を並べる。澤井製作所の出力は従来どおり +- デンソーのセッションでは品番グループの先頭にかんばんブロック(部品番号・収容数・指示数 / 次区・指示・納入日・便 / 管理番号・アイテムNo・受入)を置き、その配下に各箱をかんばん連番つきで並べる。納品番号数の行は出さず、カード番号の行も出さない - 保存はActivity Result APIの`CreateDocument("application/pdf")` - 共有は内部cacheへ一時生成し、`FileProvider`の`content://` URIと一時読み取り権限を使う - [PdfDocument](https://developer.android.com/reference/android/graphics/pdf/PdfDocument) @@ -424,13 +426,15 @@ Gradle dependency verificationとSBOM/ライセンス出力は、現行のVersio - `matching-cases.json`全ケース(`destination`つき) - 澤井製作所QR 66文字、必須フィールド、数量、枝番、空白 - モルテンQR 61文字、末尾空白の補完と正規形、収容数・納入指示日・時刻の桁検証、仕向地判定 -- Code 128業務形式、管理コード、逆順拒否、仕向地ごとの末尾3桁/4桁 +- デンソーQRのJAMA自己記述解析(ヘッダ長・項目定義・データ部の桁合計、項目構成が異なる合成ペイロード、壊れたヘッダや項目番号重複の拒否、必須項目104/112/152) +- Code 128業務形式、管理コード、逆順拒否、仕向地ごとの区切り(`4-2-4` / `4-2-3` / `6-4`) +- 仕向地ごとの品番表記(`formatPartNumber`。デンソーの10桁は`6-4`) - 状態機械の全遷移 -- 仕向地の固定、別仕向地QRの拒否、固定あり/なしでの桁数分類 +- 仕向地の固定、別仕向地QRの拒否、固定あり/なしでの桁数分類、デンソーと`JAMA`接頭辞での長さ案内なし - 自動次工程の1/3/5秒、停止条件、仮想時間 -- 重複箱の保存・グルーピング(澤井製作所はQR、モルテンはQR+Code 128) +- 重複箱の保存・グルーピング(澤井製作所とデンソーはQR、モルテンはQR+Code 128) - モルテンの納品番号ごとの箱数と累計収容数 -- PDF view modelの全項目(仕向地と納品番号ごとのブロックを含む) +- PDF view modelの全項目(仕向地、納品番号ごとのブロック、デンソーのかんばんブロックを含む) - BLE Fakeの接続・切断・fallback・timeout ### 12.2 Android instrumentation / Compose UI test diff --git a/docs/android/STATUS.md b/docs/android/STATUS.md index 9d8245a..6b530be 100644 --- a/docs/android/STATUS.md +++ b/docs/android/STATUS.md @@ -15,9 +15,9 @@ | 領域 | 確認済み(自動test・artifact検査・Pixel 7実機) | |---|---| -| Domain / matching | 純Kotlin matcher/parser、仕向地判定(澤井製作所66桁 / モルテン61桁)と仕向地ごとのCode 128形式・箱固有キー、shared fixture(`matching-cases.json`、schemaVersion 2・35ケース・`destination`つき)、JVM test、Swiftの単体/UIテストとの意図対応表([`TEST_PARITY.md`](TEST_PARITY.md)) | +| Domain / matching | 純Kotlin matcher/parser、仕向地判定(澤井製作所66桁 / モルテン61桁 / デンソーはJAMA自己記述形式の可変長かんばん)と仕向地ごとのCode 128形式・品番表記・箱固有キー、shared fixture(`matching-cases.json`、schemaVersion 2・47ケース・`destination`は`sawai`/`molten`/`denso`)、JVM test、Swiftの単体/UIテストとの意図対応表([`TEST_PARITY.md`](TEST_PARITY.md)) | | UI / navigation | Composeの照合・履歴・設定、3 destination、system/predictive backの完了・無効・cancel境界、履歴選択のActivity再生成・destination往復・compact back stack、320dp/840dp・font scale 1.3/2.0の主要操作到達、動的案内・結果のpolite live region、emulatorでのQR待機・Code 128待機・一致結果のOS force-stop後UI復元。Pixel 7ではfont scale 1.3/2.0の主要表示・操作をユーザーが承認 | -| History / settings / PDF | Room(schema v3、セッションとcheckpointの仕向地列と`MIGRATION_2_3`を含む)/DataStore、日英リソースとper-app locale双方向同期、0件破棄・名称変更・詳細・削除のapp E2E、履歴詳細とPDFのモルテン項目(納品番号ごとの箱数・累計収容数と解析全項目)、A4複数ページPDFの実render、SAF保存/専用FileProvider共有の契約test。Pixel 7では日英切替、1ページ/複数ページPDFのDownloads保存と共有先での表示、音量0/通常音量の音・触覚をユーザーが承認 | +| History / settings / PDF | Room(schema v3、セッションとcheckpointの仕向地列と`MIGRATION_2_3`を含む)/DataStore、日英リソースとper-app locale双方向同期、0件破棄・名称変更・詳細・削除のapp E2E、履歴詳細とPDFのモルテン項目(納品番号ごとの箱数・累計収容数と解析全項目)とデンソー項目(履歴詳細のかんばん解析13項目、PDFのかんばん要約と箱ごとのかんばん連番)、A4複数ページPDFの実render、SAF保存/専用FileProvider共有の契約test。Pixel 7では日英切替、1ページ/複数ページPDFのDownloads保存と共有先での表示、音量0/通常音量の音・触覚をユーザーが承認 | | Camera | CameraX/ML Kit adapter、工程別ROI、権限・lifecycle・focus・format切替の非同期境界test。Pixel 7縦画面で実ラベルのQR→Code 128一致、復帰後のCode 128、タップfocus、権限の拒否・恒久拒否・再許可、ガイド枠内外の読取境界、無関係QR拒否、不一致の表示・音・振動・非加算をユーザーが承認 | | BLE | SDK非依存の安全コア(command直列化、全設定snapshot、復元前Ready禁止、known-device store、再接続予算)、公式SDK adapter、公式native通知parser、工程別symbology制限(QR待機はQRのみ、Code 128待機はCode 128のみ)、照明の接続時OFF適用、読取チューニング(差分時のみ書込・readback確認)、診断ログの共有・保存、R8 vendor-log除去。Pixel 7 / BCST-36では検索・接続・fresh readback、QR→Code 128一致、背景復元、QR待機中のapp force-stop後の自動再接続、手動切断後の工程保持と再接続、電源OFF→ONの自動再接続、通常終了・手動切断・電源再起動後の開始前設定との一致(独立probe)、照明の初期OFFと手動ON/OFFをユーザーが承認。2026-09-05に`release` APKでBCST-36と接続し、QR→Code 128の照合完了をユーザーが確認(#56)。2026-09-06にPixel 7で読取チューニング「適用済み」と赤光約4秒、診断ログの共有・保存をユーザーが確認 | | Privacy / release | Manifest、backup/D2D除外規則、専用FileProvider、`verify-release-hardening.sh`によるAPK/依存グラフ/source検査(Fake・analytics・INTERNET・legacy Bluetooth・位置情報の不在、`:scanner:inateck`とarm64 native libraryの同梱、vendor raw-log除去、ML Kit registrar保持)。Pixel 7のnetstatsで当該UIDの通信量エントリなし | @@ -60,6 +60,18 @@ JDK/SDKがない環境ではGradle結果を推測せず、実行不能として ## 履歴 +### 2026-09-07 仕向地デンソー対応 + +3つ目の仕向地デンソーを照合の前提に加えた(Issue #106、子issue #107〜#112)。 + +- 共通fixture `matching-cases.json` を47ケースへ拡張した(澤井製作所23・モルテン11・デンソー11・仕向地なし2)。デンソーのQRは実測221桁のJAMA自己記述レコードで、印刷用画像も `denso-` の3点を追加した。`schemaVersion` は2のまま(PR #113)。 +- `core:model`の`Destination`へ`DENSO`を、`core:matching`へ`DensoKanbanQrRecord`を追加した。JAMAヘッダ(`JAMA`+版1桁+ヘッダ長4桁+前置き10桁+「項目番号3桁+桁数2桁」の並び)を解析し、項目定義の桁数の合計がデータ部の長さと一致すること、項目104(部品番号)・112(収容数)・152(かんばん連番)があることを受理条件とする。`detectDestination`はデンソーを最初に判定する(`KanbanQrRecord`の解析が寛容で`JAMA5011…`をカード番号として通すため)。`expectedQrLength`は`DENSO`でnullを返し、`TagBarcodeRecord`は`6-4`のパターンを持つ。`formatPartNumber`は仕向地引数を必須にし、デンソーの10桁を`6-4`で表記する(PR #113)。 +- 照合工程ではデンソーも既存の仕向地ロックに乗る。重複判定キーはQR全文(かんばん連番が箱ごとに違う)、箱数は品番ごとの「N箱目」で、いずれも澤井製作所と同じ経路を通る。解析できないQRの案内は、デンソーで固定済みのセッションと、未固定でも`JAMA`で始まる読取値では長さ案内をせず`INVALID_PAYLOAD`とする。予定箱数の表示と完了判定はしない(PR #114)。 +- 履歴詳細へデンソーのかんばん13項目(帳票区分・部品番号・包装・収容数・次区・指示・かんばん連番・管理番号・納入日・便・指示数・アイテムNo・受入)を出し、PDFには品番ごとのかんばん要約3行(部品番号・収容数・指示数/次区・指示・納入日・便/管理番号・アイテムNo・受入)と箱ごとのかんばん連番・管理コードを出す。仕向地名の`scan_destination_denso`・`history_destination_denso`と項目ラベル10キー(`history_form_type`・`history_packaging_code`・`history_next_process`・`history_instruction_code`・`history_kanban_serial`・`history_management_number`・`history_delivery_date`・`history_delivery_run`・`history_denso_item_number`・`history_receiving_code`)を日英へ追加した(`history_instructed_quantity`はモルテンから流用)。納品番号ごとの集計はモルテンだけのまま、澤井製作所の出力は変えていない(PR #116)。 +- Room schema は v3 のまま。仕向地は文字列で保存するため、`denso` の追加でmigrationもcheckpointの契約versionも変えていない。 + +証跡: PR #113 / #114 / #116 の実行結果。JVM test 446件、instrumentation は CI emulator(API 36)で実行。iOS側は PR #113 / #115 と履歴・PDFの #117 が対応。実かんばん・実スキャナーによるデンソー照合の実機確認は未実施であり、この記述は自動testと成果物検査の範囲を示す。 + ### 2026-09-07 仕向地モルテン対応 仕向地(澤井製作所 / モルテン)を照合の前提に加えた(Issue #84、子issue #85〜#92)。 diff --git a/docs/android/TEST_PARITY.md b/docs/android/TEST_PARITY.md index 89d64fa..cc6888b 100644 --- a/docs/android/TEST_PARITY.md +++ b/docs/android/TEST_PARITY.md @@ -2,9 +2,9 @@ 監査日: 2026-09-04。対象は [`ios/CodeMatchTests/`](../../ios/CodeMatchTests) 配下の XCTest(監査時点で71本。2026-09-06にテストクラスごとのファイルへ分割し、以下の行番号はその分割後のもの)と [`CodeMatchUITests.swift`](../../ios/CodeMatchUITests/CodeMatchUITests.swift) の UI テスト5本。件数やテスト名の一致ではなく、各テストが保証する意図を Android 側の証拠へ対応付ける。Android の `D` は同じ契約を同等の層で検査、`P` は近接する状態・部品の検査(元テスト全体の代替ではない)、`—` は Android に適用されるが証拠がない行、`N/A` は Android の現行共通仕様に含まれず対応不要と根拠リンクで確認した行を表す。 -行番号は記載時点のものであり、その後の変更(#82 / #83 / #93 / #95 / #97)で前後している。行の同定はテスト名で行うこと。2026-09-07時点のSwiftソースは単体123本・UI 6本で、監査後に追加された仕向地関連のテストは末尾の「仕向地モルテンの追加テスト(2026-09-07)」で対応付ける。 +行番号は記載時点のものであり、その後の変更(#82 / #83 / #93 / #95 / #97 / #113 / #115)で前後している。行の同定はテスト名で行うこと。2026-09-07時点のSwiftソースは単体が約155本・UI 7本で、監査後に追加された仕向地関連のテストは末尾の「仕向地モルテンの追加テスト(2026-09-07)」と「仕向地デンソーの追加テスト(2026-09-07)」で対応付ける。 -共通照合データは [`matching-cases.json`](../../shared/test-fixtures/matching-cases.json)(schemaVersion 2、35ケース、各ケースにQRの`destination`)であり、Swift はファイルを直接読み、Kotlin は test runtime classpath から読む。`src/test` は JVM テスト、`src/androidTest` は端末・エミュレーター依存の証拠である。D/P は「実カメラ読取」や「対象 BLE scanner 通信」の成功を意味しない。 +共通照合データは [`matching-cases.json`](../../shared/test-fixtures/matching-cases.json)(schemaVersion 2、47ケース、各ケースにQRの`destination`)であり、Swift はファイルを直接読み、Kotlin は test runtime classpath から読む。`src/test` は JVM テスト、`src/androidTest` は端末・エミュレーター依存の証拠である。D/P は「実カメラ読取」や「対象 BLE scanner 通信」の成功を意味しない。 ### Android 証拠ファイル(略記の正本) @@ -26,6 +26,7 @@ | `HistoryExportTextTest` | `android/core/export/src/test/kotlin/jp/rimtty/codematch/core/export/HistoryExportTextTest.kt` | | `HistoryPdfContentTest` | `android/core/export/src/test/kotlin/jp/rimtty/codematch/core/export/HistoryPdfContentTest.kt` | | `HistoryDeliveryGroupsTest` | `android/core/export/src/test/kotlin/jp/rimtty/codematch/core/export/HistoryDeliveryGroupsTest.kt` | +| `HistoryJsonExporterTest` | `android/core/export/src/test/kotlin/jp/rimtty/codematch/core/export/HistoryJsonExporterTest.kt` | | `HistoryPdfBridgeTest` | `android/app/src/test/java/jp/rimtty/codematch/history/HistoryPdfBridgeTest.kt` | | `HistoryPdfExporterInstrumentationTest` | `android/core/export/src/androidTest/kotlin/jp/rimtty/codematch/core/export/HistoryPdfExporterInstrumentationTest.kt` | | `ScanModelsTest` | `android/core/model/src/test/kotlin/jp/rimtty/codematch/core/model/ScanModelsTest.kt` | @@ -179,6 +180,31 @@ Issue #84(PR #93 / #95 / #96 / #97 / #98 / #99)で追加した Swift テス | UI 6 | Fake Bluetooth のモルテン流れで仕向地が固定され、納品番号ごとに箱が数えられる(`CodeMatchUITests::testMockBluetoothScannerMoltenFlowLocksDestination`) | `AppFlowInstrumentationTest.kt::fakeScannerMoltenFlowCountsBoxesPerDeliveryNumberAndLocksDestination` は同じ debug Fake、app navigation、ViewModel、Room を通す | D(debug Fake) | | PDF | 履歴詳細と PDF のモルテン項目(仕向地、納品番号数、納品番号ごとの箱数・累計、解析全項目)(`SessionPDFExporterTests::testFixturePayloadsAreFullRecords` + `SessionPDFExporterTests::testMoltenInstructionDateAndTimeAreFormattedForDisplay` + `SessionPDFExporterTests::testMoltenSessionListsDeliveryNoteBlockPerDeliveryNumber` + `SessionPDFExporterTests::testSawaiSessionKeepsPartNumberBlocksAndShowsDestination` + `SessionPDFExporterTests::testSessionWithoutParsableQRHasNoDestinationLine`、`CodeMatchUITests::testMockBluetoothScannerMoltenFlowLocksDestination` の履歴詳細部分) | `HistoryPdfContentTest.kt::moltenReportGroupsBoxesPerDeliveryNumberWithCumulativeQuantityAndAllFields` + `::englishMoltenReportUsesEnglishLabels` + `::legacySawaiReportIsUnchangedWhenDestinationIsNull` + `HistoryDeliveryGroupsTest.kt::instructionDateAndTimeAreFormattedAndUnexpectedValuesArePreserved` + `HistoryScreenTest.kt::moltenEntryDetailDisplaysAllParsedFields` + `::moltenGroupDetailShowsPerDeliveryNumberSummary` + `::sessionDetailAndRowShowDestination` | D | +## 仕向地デンソーの追加テスト(2026-09-07) + +Issue #106(PR #113 / #114 / #115 / #116、iOS の履歴・PDF は #117)で追加した Swift テストと、その Android 証拠。番号は上の表の続きで、Swift のファイル内位置は変わるためテスト名で同定する。 + +| # | Swift の意図(テスト) | Android の証拠 | 判定 | +|---:|---|---|:---:| +| 95 | デンソーの共通 fixture がカメラ・Bluetooth 双方の受理境界を通る(`CodeMatcherTests::testSharedDensoPairsPassBothScanBoundaries`) | `CodeMatcherTest.kt::sharedDensoFixturesPassBothScanBoundaries` + `ScanReducerTest.kt::moltenAndDensoFixturesKeepTheirPadding` | D | +| 96 | 実物のかんばんから JAMA 自己記述形式の全項目を解析する(`CodeMatcherTests::testDensoKanbanRecordParsesAllItemsFromRealPayload`) | `CodeMatcherTest.kt::densoRecordParsesAllItemsFromRealPayload` | D | +| 97 | 項目構成・ヘッダ長が違うかんばんも同じ規則で解析でき、仕向地判定がデンソーを返す(`CodeMatcherTests::testDensoKanbanRecordParsesDifferentItemLayout`) | `CodeMatcherTest.kt::densoRecordParsesADifferentItemLayout` | D | +| 98 | ヘッダ長・データ部の桁合計・必須項目・項目番号重複が崩れた読取値は受理しない(`CodeMatcherTests::testDensoKanbanRecordRejectsBrokenHeaderOrDataLength`) | `CodeMatcherTest.kt::densoRecordRejectsBrokenHeaderOrDataLength` | D | +| 99 | `6-4` の Code 128 はデンソーだけで受理する(`CodeMatcherTests::testTagBarcodeRecordAcceptsSixFourOnlyForDenso`) | `CodeMatcherTest.kt::tagValidationAllowsSixFourOnlyForDenso` | D | +| 100 | 品番の表記は仕向地依存で、デンソーの10桁は `6-4`(`CodeMatcherTests::testFormatPartNumberIsDestinationAware`) | `CodeMatcherTest.kt::formatPartNumberIsDestinationAware` | D | +| 101 | デンソーの QR→`6-4` バーコードが一致し、品番ごとの箱番号を報告する(`BluetoothScannerFlowTests::testDensoQRThenSixFourBarcodeMatches`、`::testDensoQRViaCameraShowsSixFourPartNumber`) | `ScanReducerTest.kt::densoQrThenSixFourTagMatchesAndCountsPerPartNumber` + `ScanScreenTest.kt::densoMatchResultShowsDestinationBadgeAndPlainCard` | D | +| 102 | 同じ品番の別かんばんは2箱目として計上する(`BluetoothScannerFlowTests::testDensoSecondKanbanSamePartIsCountedAsSecondBox`) | `ScanReducerTest.kt::densoSecondKanbanSamePartIsSecondBox` | D | +| 103 | 同じかんばんの読み直しは、ラベルが違っても重複(`BluetoothScannerFlowTests::testDensoRescanOfSameKanbanIsDuplicate`、`HistoryStoreTests::testDensoDuplicateRuleKeysOnQROnly`) | `ScanReducerTest.kt::densoSameKanbanIsDuplicateEvenWithAnotherTag` + `CodeMatcherTest.kt::boxIdentityIncludesTagOnlyForMolten` | D | +| 104 | デンソーの箱数は品番ごとに数える(`HistoryStoreTests::testDensoMatchCountPerPartNumber`) | `ScanReducerTest.kt::restoredDensoBoxesSeedDuplicateAndPartCounts` | D | +| 105 | デンソーで固定したセッションは澤井製作所・モルテンの QR を拒否し、その逆も拒否する(`BluetoothScannerFlowTests::testSessionLockedToDensoRejectsSawaiAndMoltenQR`、`::testSawaiSessionRejectsDensoQR`) | `ScanReducerTest.kt::densoSessionRejectsSawaiAndMoltenQrWithWrongDestination` | D | +| 106 | 澤井製作所・モルテンのセッションでは `6-4` の Code 128 を受理しない(`BluetoothScannerFlowTests::testSixFourBarcodeIsRejectedInSawaiAndMoltenSessions`) | `ScanReducerTest.kt::sawaiAndMoltenSessionsRejectSixFourTag` + `ScanSessionCoordinatorTest.kt::cameraSixFourTagGoesThroughStabilizerOnlyInDensoSession` | D | +| 107 | Code 128 待機中のデンソー QR は順序違いとして拒否(`BluetoothScannerFlowTests::testDensoQRAtBarcodeStepIsWrongOrder`) | `ScanReducerTest.kt::reverseOrderAndInvalidPayloadAreRejectedWithoutChangingState` | D | +| 108 | デンソーの不一致は計上しない(`BluetoothScannerFlowTests::testDensoMismatchIsNotCounted`) | `CodeMatcherTest.kt::sharedMatchingFixturesHaveTheSameResultsAsSwift`(デンソーの不一致 fixture 4件)+ `ScanReducerTest.kt::mismatchRemainsVisibleAndNeverProducesRecordEffect` | D | +| 109 | ViewModel 再生成時に active session からデンソーの固定を復元(`BluetoothScannerFlowTests::testDestinationIsRestoredFromActiveSessionForDenso`) | `ScanCheckpointMappingTest.kt::checkpointWithoutDestinationDerivesDensoFromAcceptedQr` + `ScanViewModelCheckpointInstrumentationTest.kt::densoDestinationLockSurvivesIsolatedDatabaseReopen` | D | +| 110 | 解析できない QR の案内は、デンソーで固定済みでも未固定の `JAMA` 接頭辞でも長さを示さない(Swift は長さ案内自体を持たないため N/A) | `ScanReducerTest.kt::densoLockedInvalidQrIsInvalidWithoutLengthHint` + `::unlockedJamaPrefixedInvalidQrIsInvalidNotIncomplete` | N/A(Android のみ) | +| UI 7 | Fake Bluetooth のデンソー流れで仕向地が固定され、品番ごとに箱が数えられる(`CodeMatchUITests::testMockBluetoothScannerDensoFlowLocksDestination`) | `AppFlowInstrumentationTest.kt::fakeScannerDensoFlowCountsBoxesPerPartNumberAndLocksDestination` は同じ debug Fake、app navigation、ViewModel、Room を通す | D(debug Fake) | +| PDF 2 | 履歴詳細のデンソー13項目(帳票区分・部品番号・包装・収容数・次区・指示・かんばん連番・管理番号・納入日・便・指示数・アイテムNo・受入)、PDF のかんばん要約3行と箱ごとのかんばん連番・管理コード、カード番号と納品番号数が出ないこと、履歴 JSON の `"destination": "denso"`。Swift 側は #117 で追加する | `HistoryPdfContentTest.kt::densoReportPrintsKanbanBlockAndBoxesPerPartNumber` + `::englishDensoReportUsesEnglishLabels` + `HistoryDeliveryGroupsTest.kt::densoEntriesProduceNoDeliveryGroups` + `HistoryJsonExporterTest.kt::densoSessionExportsItsDestinationIdAndKanbanPayloadByteForByte` + `HistoryScreenTest.kt::densoEntryDetailDisplaysAllParsedFields` + `::densoSessionDetailAndRowShowDestination` | D | + ## 残る物理・手動・未対応の証拠 2026-09-05のIssue #57で、この節に挙がる実機・手動ゲートのうち未実施のものは打ち切りとし、これ以上確認しません。打ち切りは検証成功を意味せず、`P`/`—`の分類は変更しません。一覧は[`STATUS.md`](STATUS.md)の「打ち切った確認項目」を参照してください。 @@ -203,7 +229,7 @@ Issue #84(PR #93 / #95 / #96 / #97 / #98 / #99)で追加した Swift テス ## 検証 -- 監査時点の Swift source の `func test` 数: unit 71、UI 5。2026-09-07時点では unit 118、UI 6(仕向地モルテンの追加分を含む)。fixture は JSON として schemaVersion 2、35 case、ID 重複なし、`destination` は `sawai` 22 件・`molten` 11 件・どちらでもない 2 件。 +- 監査時点の Swift source の `func test` 数: unit 71、UI 5。2026-09-07時点では unit が約155、UI 7(仕向地モルテン・デンソーの追加分を含む)。fixture は JSON として schemaVersion 2、47 case、ID 重複なし、`destination` は `sawai` 23 件・`molten` 11 件・`denso` 11 件・どれでもない 2 件。Android の JVM test は 446 件。 - Android の focused Gradle test は Android Studio の JDK と SDK を明示して実行し、次の2系統がともに `BUILD SUCCESSFUL` になった。`./gradlew :core:model:testDebugUnitTest :core:matching:testDebugUnitTest :feature:scan:testDebugUnitTest :scanner:ble:testDebugUnitTest :scanner:fake:testDebugUnitTest`、および `./gradlew :core:export:testDebugUnitTest :feature:history:testDebugUnitTest :feature:settings:testDebugUnitTest :app:testDebugUnitTest`。 - `:scanner:camera:testDebugUnitTest` は非同期境界20テストを含め `BUILD SUCCESSFUL`、`:scanner:camera:lintDebug` も `BUILD SUCCESSFUL` になった。`BundledMlKitImageDecodeTest` 3件は共有QR/Code 128画像の実decodeと誤形式拒否に成功したが、実機 camera readを意味しない。 - 2026-09-03の追加hardening後、Android JVM testは全249件が成功した。Pixel 7/API 36では通常のdebugアプリ保存領域を消去せず、`core:data` 21件、`feature:scan` 15件、`scanner:camera` 3件の計39件を実行し、失敗・skip 0だった。以前の全module instrumentation 80件、BLE変更時のfocused 36件、Android 17/API 37.1・16KB emulator 63件は別時点の記録であり、この差分全体はPRのAPI 31/36 CIで再確認する。 diff --git a/docs/ios/IMPLEMENTATION_GUIDE.md b/docs/ios/IMPLEMENTATION_GUIDE.md index 863534c..1174cad 100644 --- a/docs/ios/IMPLEMENTATION_GUIDE.md +++ b/docs/ios/IMPLEMENTATION_GUIDE.md @@ -6,12 +6,12 @@ Web版 `/Users/rimd2r/rimtty/code-match/app/page.tsx` を基準にしました 1. 正方形のQRコードを読み取る。 2. 同じカメラセッションでCode 128バーコードを読み取る。 -3. 先にQRの仕向地を判定し、その様式の固定位置から品目番号(部品番号)を取り出す。澤井製作所は66桁レコードの11〜20桁目、モルテンは61桁レコードの7〜16桁目(左詰め・空白埋め)。バーコードは`@`より前を品番とし、双方を正規化して完全一致で判定する(共通ルールは [`../PRODUCT_SPEC.md`](../PRODUCT_SPEC.md))。 +3. 先にQRの仕向地を判定し、その様式から品目番号(部品番号)を取り出す。澤井製作所は66桁レコードの11〜20桁目、モルテンは61桁レコードの7〜16桁目(左詰め・空白埋め)、デンソーはJAMA自己記述形式のかんばん(可変長。ヘッダが宣言する項目定義を解析し、項目104から10桁の部品番号を取る)。判定はデンソーを最初に試す(澤井製作所の解析が寛容で `JAMA5011...` をカード番号として通してしまうため)。バーコードは`@`より前を品番とし、双方を正規化して完全一致で判定する(共通ルールは [`../PRODUCT_SPEC.md`](../PRODUCT_SPEC.md))。 4. 最初に受理したQRで作業セッションの仕向地を固定し、以後は別の仕向地のQRを受理しない。「次のコードを照合」「QRを読み取りなおす」でも不一致でも解除せず、変更にはセッション終了を必要とする。 5. 一致は緑と成功フィードバック、不一致は赤と4回の警告音・触覚で通知する。 6. バーコード待機中にQRの取り違えへ気づいた場合は「QRを読み取りなおす」でQRだけを破棄し、同じ作業セッションのまま別のQRを読み取る。 7. 「次のコードを照合」で読み取り値とカメラ状態をリセットし、同じ作業セッションを継続する。 -8. 一致した場合だけコードと照合時刻を記録し、照合画面上端の件数へ加算する。澤井製作所は品番ごと、モルテンは納品番号ごとに箱を数え、モルテンでは収容数の累計も示す。 +8. 一致した場合だけコードと照合時刻を記録し、照合画面上端の件数へ加算する。澤井製作所とデンソーは品番ごと、モルテンは納品番号ごとに箱を数え、モルテンでは収容数の累計も示す。重複の判定キーは澤井製作所とデンソーがQR全文(デンソーはかんばん連番が箱ごとに違う)、モルテンがQR+Code 128。 9. セッション終了後も、履歴タブから開始・終了時刻、一致件数、コードを確認する。 10. 任意設定で、一致結果を1秒、3秒、または5秒表示した後に次の照合を自動開始する。初期設定はOFFとする。 @@ -26,8 +26,8 @@ Web版 `/Users/rimd2r/rimtty/code-match/app/page.tsx` を基準にしました | History UI | `HistoryScreen.swift` | セッション一覧と一致コードの詳細表示 | | State | `ScannerViewModel.swift` | QR→バーコード→結果の状態機械、誤読抑制、リセット | | Camera | `CameraScanner.swift` | 権限、AVCaptureSession、メタデータ、フォーカス | -| Domain | `ScanModels.swift` | ステップ、結果、仕向地判定(`Destination`)、レコード解析(`KanbanQRRecord` / `MoltenQRRecord`)、箱固有キー(`BoxIdentity`)、正規化と完全一致 | -| History | `HistoryModels.swift`, `HistoryStore.swift` | セッションモデルと仕向地の固定、JSON永続化、件数更新、納品番号ごとの箱数・累計収容数(`DeliveryBoxSummary` / `DeliveryGroup`) | +| Domain | `ScanModels.swift` | ステップ、結果、仕向地判定(`Destination`)、レコード解析(`KanbanQRRecord` / `MoltenQRRecord` / `DensoKanbanRecord`)、箱固有キー(`BoxIdentity`)、仕向地ごとの品番表記(`CodeMatcher.format(partNumber:destination:)`)、正規化と完全一致 | +| History | `HistoryModels.swift`, `HistoryStore.swift` | セッションモデルと仕向地の固定、JSON永続化、件数更新、納品番号ごとの箱数・累計収容数(`DeliveryBoxSummary` / `DeliveryGroup`)、デンソーは澤井製作所と同じ品番ごとの箱数経路 | | Feedback | `FeedbackPlayer.swift` | 成功・失敗の音と触覚 | カメラセッションは専用Serial Queueで開始・停止し、UI状態の更新だけMainActorへ戻します。一致した読み取り値だけをApplication Support内のJSONへ保存し、完全ファイル保護を設定します。カメラ映像と不一致の値は保存しません。 @@ -99,10 +99,16 @@ Console.appで端末を選び、検索欄に `subsystem:jp.rimtty.CodeMatch` を - [ ] QRの枝番が空白でも品目番号を抽出できる - [ ] モルテンの61桁QR(末尾空白が落ちた57〜61桁の読取値を含む)を受理し、部品番号を抽出できる - [ ] モルテンのセッションでは`4-2-3`のCode 128(例: PAF1-15-422@0NKD3C)を受理し、澤井製作所のセッションでは受理しない -- [ ] 仕向地を固定した後に別の仕向地のQRを読むと、カメラ・Bluetoothとも「このセッションは仕向地「◯◯」で照合中です。…」と案内し、工程と件数が変わらない +- [ ] デンソーのかんばんQR(JAMA自己記述形式、実物は221桁)を受理し、項目104から部品番号を抽出できる +- [ ] デンソーのセッションでは`6-4`のCode 128(例: 860150-7722@1DZ50O)を受理し、品番を `860150-7722` と表記する。澤井製作所・モルテンのセッションでは`6-4`を受理しない +- [ ] 仕向地を固定した後に別の仕向地のQRを読むと、カメラ・Bluetoothとも「このセッションは仕向地「◯◯」で照合中です。…」と案内し、工程と件数が変わらない(デンソーで固定した場合は「デンソー」と出る) - [ ] 同じ納品番号の別ラベルを読むと2箱目として計上され、累計の収容数が加算される -- [ ] 同じ箱(同じQRと同じCode 128)を読み直すと重複になり、件数と履歴が増えない +- [ ] デンソーで同じ品番の別のかんばん(かんばん連番違い)を読むと「この品番は本セッションで2箱目」として計上される +- [ ] 同じ箱(同じQRと同じCode 128)を読み直すと重複になり、件数と履歴が増えない。デンソーは同じかんばんならラベルが違っても重複になる - [ ] 履歴とPDFに仕向地が出て、モルテンでは納品番号数、納品番号ごとの箱数・累計、受注者・部品番号・納品番号・納入先・TYロケーション・供給先・収容数・納入指示日(JUMP)・時刻を確認できる +- [ ] デンソーの履歴詳細に帳票区分・部品番号・包装・収容数・次区・指示・かんばん連番・管理番号・納入日・便・指示数・アイテムNo・受入が出る +- [ ] デンソーのPDFに品番ごとのかんばん要約(部品番号・収容数・指示数/次区・指示・納入日・便/管理番号・アイテムNo・受入)と、箱ごとのかんばん連番・管理コードが出る。カード番号と納品番号数は出ない +- [ ] 履歴のJSON書き出しでデンソーのセッションが `"destination": "denso"` になる - [ ] リセット後に値と結果が残らない(仕向地は同じセッション中は固定されたまま) - [ ] 自動「次の照合」は初期設定がOFFで、設定画面と照合セッション中の両方からON/OFFできる - [ ] 自動「次の照合」をONにすると、一致時だけ設定した1秒、3秒、または5秒の残り時間が表示され、0秒後に次のQR読み取りが始まる