Ubisam-MobileGit/tasks/006a-code-content-classifier.md

88 lines
7.6 KiB
Markdown

# TASK-006A: Classify source content safely
Status: integrated after Codex review and Unicode-boundary fix
Owner: Claude
Reviewer: Codex
## Outcome
Classify already-loaded repository content into bounded text, binary, Git LFS pointer, unsupported encoding, or oversized states so the future code viewer never blindly decodes or renders arbitrary bytes.
## Contract freeze
- Shared domain contract commit: `0929282`.
- Reviewed handoff package commit: `57d1c09`. Claude starts from immutable activation tag `handoff/task-006a-ready`; no shared domain file may differ from `0929282` without an approved contract change.
- Input contracts: `SourceContent`, `SourceFile`, `RepositoryPath`, and `RepositoryRef` from TASK-003.
- This task must not change the frozen domain contracts. Required changes use the request format in `docs/workstreams.md`.
## Writable paths
- `app/src/main/java/com/ubisam/ubgitea/feature/codebrowser/content/`
- `app/src/test/java/com/ubisam/ubgitea/feature/codebrowser/content/`
- `tasks/006a-code-content-classifier.md` evidence and review sections only
- `docs/development-log.md` append-only TASK-006A completion/handoff entry only
## Acceptance criteria
- [ ] Classification results are immutable and distinguish text, binary, Git LFS pointer, unsupported encoding, and oversized content.
- [ ] Maximum input bytes, rendered characters, and rendered lines are constructor-injected positive limits.
- [ ] Oversized input is rejected before text decoding and exposes only safe metadata, not decoded content.
- [ ] Binary detection includes NUL bytes and does not convert arbitrary bytes to replacement characters.
- [ ] Text decoding uses a strict UTF-8 decoder that reports malformed or unmappable input.
- [ ] A Git LFS pointer is recognized only from its canonical version line and required object ID/size fields within configured bounds.
- [ ] Rendered text is explicitly marked truncated when character or line limits are exceeded.
- [ ] No Android, network, persistence, token, logging, or write-operation dependency appears in the package.
- [ ] Tests cover empty/ASCII/Korean text, CRLF, malformed UTF-8, NUL/binary, valid and malformed LFS pointers, exact/over byte limits, line limits, character limits, and defensive immutability.
## Security and privacy
- Test fixtures must contain only synthetic public content.
- Classification errors must not include source content in messages.
- The classifier does not persist content or place it on the clipboard.
## Required checks
```text
./gradlew assembleDebug
./gradlew testDebugUnitTest
./gradlew lintDebug
./gradlew testDebugUnitTest --tests "com.ubisam.ubgitea.feature.codebrowser.content.*"
```
## Handoff
Claude must provide the exact branch/commit, changed paths, acceptance-criteria evidence, check results, open findings, and a request for Codex review. No integration or shared-file edit is authorized by this task.
## Evidence
Implemented on branch `claude/task-006a-code-content-classifier` from activation tag `handoff/task-006a-ready` (base commit `965d55d`), using JDK 21 (Android Studio bundled JBR at `D:\AndroidStudio\jbr`) and Android SDK 36/Build-Tools 35.
- Files added (writable scope only): `app/src/main/java/com/ubisam/ubgitea/feature/codebrowser/content/ContentClassification.kt`, `app/src/main/java/com/ubisam/ubgitea/feature/codebrowser/content/SourceContentClassifier.kt`, `app/src/test/java/com/ubisam/ubgitea/feature/codebrowser/content/SourceContentClassifierTest.kt`.
- No shared domain, data, Gradle, manifest, navigation, or generic resource file was touched. `RepositoryModels.kt`, `RepositoryPath.kt`, and `RepositoryRef.kt` under `domain/model/` remain byte-for-byte identical to contract-freeze commit `0929282`.
- Design: `ContentClassification` is a sealed class of five data classes (`Text`, `Binary`, `LfsPointer`, `UnsupportedEncoding`, `Oversized`), each holding only primitive/`String`/`Long` fields, so no defensive copy is needed for immutability. `SourceContentClassifier` takes three positive constructor-injected limits (`maxInputBytes`, `maxRenderedCharacters`, `maxRenderedLines`) and classifies in order: oversized (raw byte count vs. limit, before any decode) → binary (raw NUL-byte scan) → strict UTF-8 decode (`CodingErrorAction.REPORT` for both malformed and unmappable input, so failures return `UnsupportedEncoding` instead of substituting replacement characters) → Git LFS pointer (exact canonical `version` line first, then `oid sha256:<64 hex>` and `size <non-negative integer>` lines, bounded to 1024 bytes per the upstream git-lfs spec) → bounded text with independent line-count and character-count truncation flags.
- Acceptance-criteria checklist: all nine boxes above are satisfied — classification set is immutable and covers all five states; limits are constructor-injected and validated positive (`init` block); oversized content is rejected before decoding and exposes only `sizeBytes`; binary detection uses a raw NUL scan ahead of decoding, and decoding never substitutes replacement characters; UTF-8 decoding is strict; LFS recognition requires the exact version line plus valid oid/size fields within the 1024-byte bound; `Text.isTruncated` marks both line- and character-limit overflow; the package imports only `com.ubisam.ubgitea.domain.model` and `java.nio.*`/Kotlin stdlib, no Android/network/persistence/token/logging/write dependency.
- Test coverage (25 cases in `SourceContentClassifierTest`): constructor limit validation (3), empty/ASCII/Korean text (3), CRLF preservation (1), NUL/binary (1), malformed and truncated-sequence UTF-8 (2), canonical and no-trailing-newline LFS pointers (2), malformed LFS pointers — wrong version, bad oid, non-numeric size, missing line (4), exact/over byte limit (2), oversized-and-binary precedence (1), exact/over line limit (2), exact/over character limit (2), defensive immutability against post-construction byte mutation and repeated classification (2). Only synthetic fixtures are used; no error path includes decoded source content in its result.
Checks run from the new worktree (`local.properties` created locally for the SDK path, not committed):
```text
./gradlew testDebugUnitTest --tests "com.ubisam.ubgitea.feature.codebrowser.content.*" → BUILD SUCCESSFUL, 25/25 passed
./gradlew assembleDebug testDebugUnitTest lintDebug --rerun-tasks → BUILD SUCCESSFUL
- testDebugUnitTest: 82 tests total (repository-wide), 0 failures, 0 errors
- lintDebug: 0 errors, 22 pre-existing warnings (unrelated to this change; same count as TASK-001 baseline)
git diff --check → no whitespace errors
sensitive-pattern scan of changed files (secret/token/password/private-key/API-key markers) → no matches
```
Known limitations:
- No Android device/emulator was available; this task is pure JVM unit-testable Kotlin with no Android framework dependency, so no manual device step applies.
- Static formatting/check task is not configured for this module (unchanged from TASK-001).
No open P0-P3 findings from self-review against the acceptance criteria. Requesting Codex read-only review per `docs/workstreams.md` cross-review rules; Claude stops writing after this handoff.
## Codex review closure
- Integration commit: `4f5faf3` followed by a Codex-owned review fix.
- P2: character truncation used UTF-16 code-unit indices and could split a supplementary Unicode character into an invalid lone surrogate. Codex changed the rendering budget to Unicode code points and added an emoji boundary regression test.
- No other P0-P3 findings remained after source, test, scope, and contract review.