Ubisam-MobileGit/tasks/006a-code-content-classifier.md
choiminyeong 4f5faf373a Implement TASK-006A source content classifier
Pure-Kotlin classifier for feature/codebrowser/content/ that turns
already-loaded SourceContent bytes into bounded text, binary, Git LFS
pointer, unsupported-encoding, or oversized results with caller-supplied
positive byte/character/line limits, per handoff/task-006a-ready.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 09:48:39 +09:00

7.1 KiB

TASK-006A: Classify source content safely

Status: implemented by Claude; pending Codex read-only review 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

./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):

./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.